mirror of
				https://gitea.com/actions/setup-node.git
				synced 2025-10-29 15:52:42 +08:00 
			
		
		
		
	Detect cached folders from multiple directories (#735)
* Add project-dir * Fix find lock file * Remove package-dir input * format & resolve conflicts * Add unit tests * build dist * Apply change request fixes * handle non-dir cache-dependency-path * bump cache version * run checks * Handle globs in cacheDependencyPath * refactor, introduce `cacheDependencyPathToProjectsDirectories` it is necessary for the next PR related yarn optimization * Changes requests * Apply fixes * review fixes * add e2e * Add unique * review updates * review updates second stage * Review fixes 3 * imporve e2e tests
This commit is contained in:
		
							
								
								
									
										27
									
								
								.github/workflows/e2e-cache.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/e2e-cache.yml
									
									
									
									
										vendored
									
									
								
							| @@ -134,3 +134,30 @@ jobs: | ||||
|       - name: Verify node and yarn | ||||
|         run: __tests__/verify-node.sh "${{ matrix.node-version }}" | ||||
|         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 | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import os from 'os'; | ||||
| import * as fs from 'fs'; | ||||
| import fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import * as core from '@actions/core'; | ||||
| import * as io from '@actions/io'; | ||||
| import * as auth from '../src/authutil'; | ||||
| import * as cacheUtils from '../src/cache-utils'; | ||||
|  | ||||
| let rcFile: string; | ||||
|  | ||||
|   | ||||
| @@ -32,13 +32,13 @@ describe('cache-restore', () => { | ||||
|  | ||||
|   function findCacheFolder(command: string) { | ||||
|     switch (command) { | ||||
|       case utils.supportedPackageManagers.npm.getCacheFolderCommand: | ||||
|       case 'npm config get cache': | ||||
|         return npmCachePath; | ||||
|       case utils.supportedPackageManagers.pnpm.getCacheFolderCommand: | ||||
|       case 'pnpm store path --silent': | ||||
|         return pnpmCachePath; | ||||
|       case utils.supportedPackageManagers.yarn1.getCacheFolderCommand: | ||||
|       case 'yarn cache dir': | ||||
|         return yarn1CachePath; | ||||
|       case utils.supportedPackageManagers.yarn2.getCacheFolderCommand: | ||||
|       case 'yarn config get cacheFolder': | ||||
|         return yarn2CachePath; | ||||
|       default: | ||||
|         return 'packge/not/found'; | ||||
| @@ -108,7 +108,7 @@ describe('cache-restore', () => { | ||||
|     it.each([['npm7'], ['npm6'], ['pnpm6'], ['yarn1'], ['yarn2'], ['random']])( | ||||
|       'Throw an error because %s is not supported', | ||||
|       async packageManager => { | ||||
|         await expect(restoreCache(packageManager)).rejects.toThrow( | ||||
|         await expect(restoreCache(packageManager, '')).rejects.toThrow( | ||||
|           `Caching for '${packageManager}' is not supported` | ||||
|         ); | ||||
|       } | ||||
| @@ -132,7 +132,7 @@ describe('cache-restore', () => { | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|         await restoreCache(packageManager); | ||||
|         await restoreCache(packageManager, ''); | ||||
|         expect(hashFilesSpy).toHaveBeenCalled(); | ||||
|         expect(infoSpy).toHaveBeenCalledWith( | ||||
|           `Cache restored from key: node-cache-${platform}-${packageManager}-${fileHash}` | ||||
| @@ -163,7 +163,7 @@ describe('cache-restore', () => { | ||||
|         }); | ||||
|  | ||||
|         restoreCacheSpy.mockImplementationOnce(() => undefined); | ||||
|         await restoreCache(packageManager); | ||||
|         await restoreCache(packageManager, ''); | ||||
|         expect(hashFilesSpy).toHaveBeenCalled(); | ||||
|         expect(infoSpy).toHaveBeenCalledWith( | ||||
|           `${packageManager} cache is not found` | ||||
|   | ||||
| @@ -107,18 +107,20 @@ describe('run', () => { | ||||
|   describe('Validate unchanged cache is not saved', () => { | ||||
|     it('should not save cache for yarn1', async () => { | ||||
|       inputs['cache'] = 'yarn'; | ||||
|       getStateSpy.mockImplementation(() => yarnFileHash); | ||||
|       getCommandOutputSpy | ||||
|         .mockImplementationOnce(() => '1.2.3') | ||||
|         .mockImplementationOnce(() => `${commonPath}/yarn1`); | ||||
|       getStateSpy.mockImplementation(key => | ||||
|         key === State.CachePrimaryKey || key === State.CacheMatchedKey | ||||
|           ? yarnFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|  | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn1`); | ||||
|       expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 1.2.3'); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(infoSpy).toHaveBeenCalledWith( | ||||
|         `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 () => { | ||||
|       inputs['cache'] = 'yarn'; | ||||
|       getStateSpy.mockImplementation(() => yarnFileHash); | ||||
|       getCommandOutputSpy | ||||
|         .mockImplementationOnce(() => '2.2.3') | ||||
|         .mockImplementationOnce(() => `${commonPath}/yarn2`); | ||||
|       getStateSpy.mockImplementation(key => | ||||
|         key === State.CachePrimaryKey || key === State.CacheMatchedKey | ||||
|           ? yarnFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|  | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn2`); | ||||
|       expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 2.2.3'); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(infoSpy).toHaveBeenCalledWith( | ||||
|         `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 () => { | ||||
|       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`); | ||||
|  | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`); | ||||
|       expect(infoSpy).toHaveBeenCalledWith( | ||||
|         `Cache hit occurred on the primary key ${npmFileHash}, not saving cache.` | ||||
|       ); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(setFailedSpy).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should not save cache for pnpm', async () => { | ||||
|       inputs['cache'] = 'pnpm'; | ||||
|       getStateSpy.mockImplementation(() => pnpmFileHash); | ||||
|       getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/pnpm`); | ||||
|       getStateSpy.mockImplementation(key => | ||||
|         key === State.CachePrimaryKey || key === State.CacheMatchedKey | ||||
|           ? yarnFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|  | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`pnpm path is ${commonPath}/pnpm`); | ||||
|       expect(infoSpy).toHaveBeenCalledWith( | ||||
|         `Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.` | ||||
|       ); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(setFailedSpy).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
| @@ -183,24 +192,22 @@ describe('run', () => { | ||||
|   describe('action saves the cache', () => { | ||||
|     it('saves cache from yarn 1', async () => { | ||||
|       inputs['cache'] = 'yarn'; | ||||
|       getStateSpy.mockImplementation((name: string) => { | ||||
|         if (name === State.CacheMatchedKey) { | ||||
|           return yarnFileHash; | ||||
|         } else { | ||||
|           return npmFileHash; | ||||
|         } | ||||
|       }); | ||||
|       getCommandOutputSpy | ||||
|         .mockImplementationOnce(() => '1.2.3') | ||||
|         .mockImplementationOnce(() => `${commonPath}/yarn1`); | ||||
|       getStateSpy.mockImplementation((key: string) => | ||||
|         key === State.CacheMatchedKey | ||||
|           ? yarnFileHash | ||||
|           : key === State.CachePrimaryKey | ||||
|           ? npmFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|  | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn1`); | ||||
|       expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 1.2.3'); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(infoSpy).not.toHaveBeenCalledWith( | ||||
|         `Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.` | ||||
|       ); | ||||
| @@ -213,24 +220,22 @@ describe('run', () => { | ||||
|  | ||||
|     it('saves cache from yarn 2', async () => { | ||||
|       inputs['cache'] = 'yarn'; | ||||
|       getStateSpy.mockImplementation((name: string) => { | ||||
|         if (name === State.CacheMatchedKey) { | ||||
|           return yarnFileHash; | ||||
|         } else { | ||||
|           return npmFileHash; | ||||
|         } | ||||
|       }); | ||||
|       getCommandOutputSpy | ||||
|         .mockImplementationOnce(() => '2.2.3') | ||||
|         .mockImplementationOnce(() => `${commonPath}/yarn2`); | ||||
|       getStateSpy.mockImplementation((key: string) => | ||||
|         key === State.CacheMatchedKey | ||||
|           ? yarnFileHash | ||||
|           : key === State.CachePrimaryKey | ||||
|           ? npmFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|  | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn2`); | ||||
|       expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 2.2.3'); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(infoSpy).not.toHaveBeenCalledWith( | ||||
|         `Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.` | ||||
|       ); | ||||
| @@ -243,21 +248,22 @@ describe('run', () => { | ||||
|  | ||||
|     it('saves cache from npm', async () => { | ||||
|       inputs['cache'] = 'npm'; | ||||
|       getStateSpy.mockImplementation((name: string) => { | ||||
|         if (name === State.CacheMatchedKey) { | ||||
|           return npmFileHash; | ||||
|         } else { | ||||
|           return yarnFileHash; | ||||
|         } | ||||
|       }); | ||||
|       getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`); | ||||
|       getStateSpy.mockImplementation((key: string) => | ||||
|         key === State.CacheMatchedKey | ||||
|           ? npmFileHash | ||||
|           : key === State.CachePrimaryKey | ||||
|           ? yarnFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|  | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(infoSpy).not.toHaveBeenCalledWith( | ||||
|         `Cache hit occurred on the primary key ${npmFileHash}, not saving cache.` | ||||
|       ); | ||||
| @@ -270,21 +276,22 @@ describe('run', () => { | ||||
|  | ||||
|     it('saves cache from pnpm', async () => { | ||||
|       inputs['cache'] = 'pnpm'; | ||||
|       getStateSpy.mockImplementation((name: string) => { | ||||
|         if (name === State.CacheMatchedKey) { | ||||
|           return pnpmFileHash; | ||||
|         } else { | ||||
|           return npmFileHash; | ||||
|         } | ||||
|       }); | ||||
|       getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/pnpm`); | ||||
|       getStateSpy.mockImplementation((key: string) => | ||||
|         key === State.CacheMatchedKey | ||||
|           ? pnpmFileHash | ||||
|           : key === State.CachePrimaryKey | ||||
|           ? npmFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|  | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`pnpm path is ${commonPath}/pnpm`); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(infoSpy).not.toHaveBeenCalledWith( | ||||
|         `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 () => { | ||||
|       inputs['cache'] = 'npm'; | ||||
|       getStateSpy.mockImplementation((name: string) => { | ||||
|         if (name === State.CacheMatchedKey) { | ||||
|           return npmFileHash; | ||||
|         } else { | ||||
|           return yarnFileHash; | ||||
|         } | ||||
|       }); | ||||
|       getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`); | ||||
|       getStateSpy.mockImplementation((key: string) => | ||||
|         key === State.CacheMatchedKey | ||||
|           ? npmFileHash | ||||
|           : key === State.CachePrimaryKey | ||||
|           ? yarnFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|       saveCacheSpy.mockImplementation(() => { | ||||
|         return -1; | ||||
|       }); | ||||
| @@ -312,9 +320,9 @@ describe('run', () => { | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(infoSpy).not.toHaveBeenCalledWith( | ||||
|         `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 () => { | ||||
|       inputs['cache'] = 'npm'; | ||||
|       getStateSpy.mockImplementation((name: string) => { | ||||
|         if (name === State.CacheMatchedKey) { | ||||
|           return npmFileHash; | ||||
|         } else { | ||||
|           return yarnFileHash; | ||||
|         } | ||||
|       }); | ||||
|       getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`); | ||||
|       getStateSpy.mockImplementation((key: string) => | ||||
|         key === State.CacheMatchedKey | ||||
|           ? npmFileHash | ||||
|           : key === State.CachePrimaryKey | ||||
|           ? yarnFileHash | ||||
|           : key === State.CachePaths | ||||
|           ? '["/foo/bar"]' | ||||
|           : 'not expected' | ||||
|       ); | ||||
|       saveCacheSpy.mockImplementation(() => { | ||||
|         throw new cache.ValidationError('Validation failed'); | ||||
|       }); | ||||
| @@ -342,9 +351,9 @@ describe('run', () => { | ||||
|       await run(); | ||||
|  | ||||
|       expect(getInputSpy).toHaveBeenCalled(); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(2); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); | ||||
|       expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`); | ||||
|       expect(getStateSpy).toHaveBeenCalledTimes(3); | ||||
|       expect(getCommandOutputSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(debugSpy).toHaveBeenCalledTimes(0); | ||||
|       expect(infoSpy).not.toHaveBeenCalledWith( | ||||
|         `Cache hit occurred on the primary key ${npmFileHash}, not saving cache.` | ||||
|       ); | ||||
|   | ||||
| @@ -2,7 +2,17 @@ import * as core from '@actions/core'; | ||||
| import * as cache from '@actions/cache'; | ||||
| import path from 'path'; | ||||
| 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', () => { | ||||
|   const versionYarn1 = '1.2.3'; | ||||
| @@ -30,7 +40,7 @@ describe('cache-utils', () => { | ||||
|     it.each<[string, PackageManagerInfo | null]>([ | ||||
|       ['npm', utils.supportedPackageManagers.npm], | ||||
|       ['pnpm', utils.supportedPackageManagers.pnpm], | ||||
|       ['yarn', utils.supportedPackageManagers.yarn1], | ||||
|       ['yarn', utils.supportedPackageManagers.yarn], | ||||
|       ['yarn1', null], | ||||
|       ['yarn2', null], | ||||
|       ['npm7', null] | ||||
| @@ -72,4 +82,261 @@ describe('cache-utils', () => { | ||||
|     jest.resetAllMocks(); | ||||
|     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` | ||||
|         ]); | ||||
|       } | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										18
									
								
								__tests__/mock/glob-mock.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								__tests__/mock/glob-mock.test.ts
									
									
									
									
									
										Normal 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']); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										29
									
								
								__tests__/mock/glob-mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								__tests__/mock/glob-mock.ts
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										48
									
								
								__tests__/prepare-subprojects.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										48
									
								
								__tests__/prepare-subprojects.sh
									
									
									
									
									
										Executable 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 | ||||
| @@ -25,7 +25,7 @@ inputs: | ||||
|     description: 'Used to specify a package manager for caching in the default directory. Supported values: npm, yarn, pnpm.' | ||||
|   cache-dependency-path: | ||||
|     description: 'Used to specify the path to a dependency file: package-lock.json, yarn.lock, etc. Supports wildcards or a list of file names for caching multiple dependencies.' | ||||
| # TODO: add input to control forcing to pull from cloud or dist.  | ||||
| # TODO: add input to control forcing to pull from cloud or dist. | ||||
| #       escape valve for someone having issues or needing the absolute latest which isn't cached yet | ||||
| outputs: | ||||
|   cache-hit:  | ||||
|   | ||||
							
								
								
									
										1792
									
								
								dist/cache-save/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1792
									
								
								dist/cache-save/index.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2289
									
								
								dist/setup/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2289
									
								
								dist/setup/index.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,14 +6,14 @@ import fs from 'fs'; | ||||
|  | ||||
| import {State} from './constants'; | ||||
| import { | ||||
|   getCacheDirectoryPath, | ||||
|   getCacheDirectories, | ||||
|   getPackageManagerInfo, | ||||
|   PackageManagerInfo | ||||
| } from './cache-utils'; | ||||
|  | ||||
| export const restoreCache = async ( | ||||
|   packageManager: string, | ||||
|   cacheDependencyPath?: string | ||||
|   cacheDependencyPath: string | ||||
| ) => { | ||||
|   const packageManagerInfo = await getPackageManagerInfo(packageManager); | ||||
|   if (!packageManagerInfo) { | ||||
| @@ -21,10 +21,11 @@ export const restoreCache = async ( | ||||
|   } | ||||
|   const platform = process.env.RUNNER_OS; | ||||
|  | ||||
|   const cachePath = await getCacheDirectoryPath( | ||||
|   const cachePaths = await getCacheDirectories( | ||||
|     packageManagerInfo, | ||||
|     packageManager | ||||
|     cacheDependencyPath | ||||
|   ); | ||||
|   core.saveState(State.CachePaths, cachePaths); | ||||
|   const lockFilePath = cacheDependencyPath | ||||
|     ? cacheDependencyPath | ||||
|     : findLockFile(packageManagerInfo); | ||||
| @@ -41,7 +42,7 @@ export const restoreCache = async ( | ||||
|  | ||||
|   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)); | ||||
|  | ||||
|   if (!cacheKey) { | ||||
| @@ -56,6 +57,7 @@ export const restoreCache = async ( | ||||
| const findLockFile = (packageManager: PackageManagerInfo) => { | ||||
|   const lockFiles = packageManager.lockFilePatterns; | ||||
|   const workspace = process.env.GITHUB_WORKSPACE!; | ||||
|  | ||||
|   const rootContent = fs.readdirSync(workspace); | ||||
|  | ||||
|   const lockFile = lockFiles.find(item => rootContent.includes(item)); | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import * as core from '@actions/core'; | ||||
| import * as cache from '@actions/cache'; | ||||
| import fs from 'fs'; | ||||
| 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 | ||||
| // @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 state = core.getState(State.CacheMatchedKey); | ||||
|   const primaryKey = core.getState(State.CachePrimaryKey); | ||||
|   const cachePaths = JSON.parse(core.getState(State.CachePaths) || '[]'); | ||||
|  | ||||
|   const packageManagerInfo = await getPackageManagerInfo(packageManager); | ||||
|   if (!packageManagerInfo) { | ||||
| @@ -31,14 +31,12 @@ const cachePackages = async (packageManager: string) => { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const cachePath = await getCacheDirectoryPath( | ||||
|     packageManagerInfo, | ||||
|     packageManager | ||||
|   ); | ||||
|  | ||||
|   if (!fs.existsSync(cachePath)) { | ||||
|   if (cachePaths.length === 0) { | ||||
|     // TODO: core.getInput has a bug - it can return undefined despite its definition (tests only?) | ||||
|     //       export declare function getInput(name: string, options?: InputOptions): string; | ||||
|     const cacheDependencyPath = core.getInput('cache-dependency-path') || ''; | ||||
|     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; | ||||
|   } | ||||
|  | ||||
|   const cacheId = await cache.saveCache([cachePath], primaryKey); | ||||
|   const cacheId = await cache.saveCache(cachePaths, primaryKey); | ||||
|   if (cacheId == -1) { | ||||
|     return; | ||||
|   } | ||||
|   | ||||
| @@ -1,40 +1,79 @@ | ||||
| import * as core from '@actions/core'; | ||||
| import * as exec from '@actions/exec'; | ||||
| import * as cache from '@actions/cache'; | ||||
|  | ||||
| type SupportedPackageManagers = { | ||||
|   [prop: string]: PackageManagerInfo; | ||||
| }; | ||||
| 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>; | ||||
|   getCacheFolderCommand: 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'], | ||||
|     getCacheFolderCommand: 'npm config get cache' | ||||
|     getCacheFolderPath: () => | ||||
|       getCommandOutputNotEmpty( | ||||
|         'npm config get cache', | ||||
|         'Could not get npm cache folder path' | ||||
|       ) | ||||
|   }, | ||||
|   pnpm: { | ||||
|     name: 'pnpm', | ||||
|     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'], | ||||
|     getCacheFolderCommand: 'yarn cache dir' | ||||
|   }, | ||||
|   yarn2: { | ||||
|     lockFilePatterns: ['yarn.lock'], | ||||
|     getCacheFolderCommand: 'yarn config get cacheFolder' | ||||
|     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) => { | ||||
| export const getCommandOutput = async ( | ||||
|   toolCommand: string, | ||||
|   cwd?: string | ||||
| ): Promise<string> => { | ||||
|   let {stdout, stderr, exitCode} = await exec.getExecOutput( | ||||
|     toolCommand, | ||||
|     undefined, | ||||
|     {ignoreReturnCode: true} | ||||
|     {ignoreReturnCode: true, ...(cwd && {cwd})} | ||||
|   ); | ||||
|  | ||||
|   if (exitCode) { | ||||
| @@ -47,16 +86,15 @@ export const getCommandOutput = async (toolCommand: string) => { | ||||
|   return stdout.trim(); | ||||
| }; | ||||
|  | ||||
| const getPackageManagerVersion = async ( | ||||
|   packageManager: string, | ||||
|   command: string | ||||
| ) => { | ||||
|   const stdOut = await getCommandOutput(`${packageManager} ${command}`); | ||||
|  | ||||
| export const getCommandOutputNotEmpty = async ( | ||||
|   toolCommand: string, | ||||
|   error: string, | ||||
|   cwd?: string | ||||
| ): Promise<string> => { | ||||
|   const stdOut = getCommandOutput(toolCommand, cwd); | ||||
|   if (!stdOut) { | ||||
|     throw new Error(`Could not retrieve version of ${packageManager}`); | ||||
|     throw new Error(error); | ||||
|   } | ||||
|  | ||||
|   return stdOut; | ||||
| }; | ||||
|  | ||||
| @@ -66,35 +104,102 @@ export const getPackageManagerInfo = async (packageManager: string) => { | ||||
|   } else if (packageManager === 'pnpm') { | ||||
|     return supportedPackageManagers.pnpm; | ||||
|   } else if (packageManager === 'yarn') { | ||||
|     const yarnVersion = await getPackageManagerVersion('yarn', '--version'); | ||||
|  | ||||
|     core.debug(`Consumed yarn version is ${yarnVersion}`); | ||||
|  | ||||
|     if (yarnVersion.startsWith('1.')) { | ||||
|       return supportedPackageManagers.yarn1; | ||||
|     } else { | ||||
|       return supportedPackageManagers.yarn2; | ||||
|     } | ||||
|     return supportedPackageManagers.yarn; | ||||
|   } else { | ||||
|     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, | ||||
|   packageManager: string | ||||
| ) => { | ||||
|   const stdOut = await getCommandOutput( | ||||
|     packageManagerInfo.getCacheFolderCommand | ||||
|   cacheDependencyPath: string | ||||
| ): Promise<string[]> => { | ||||
|   const projectDirectories = await getProjectDirectoriesFromCacheDependencyPath( | ||||
|     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 | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   core.debug(`${packageManager} path is ${stdOut}`); | ||||
|  | ||||
|   return stdOut.trim(); | ||||
|   return getCacheDirectoriesForRootProject(packageManagerInfo); | ||||
| }; | ||||
|  | ||||
| export function isGhes(): boolean { | ||||
|   | ||||
| @@ -6,7 +6,8 @@ export enum LockType { | ||||
|  | ||||
| export enum State { | ||||
|   CachePrimaryKey = 'CACHE_KEY', | ||||
|   CacheMatchedKey = 'CACHE_RESULT' | ||||
|   CacheMatchedKey = 'CACHE_RESULT', | ||||
|   CachePaths = 'CACHE_PATHS' | ||||
| } | ||||
|  | ||||
| export enum Outputs { | ||||
|   | ||||
| @@ -61,3 +61,12 @@ async function getToolVersion(tool: string, options: string[]) { | ||||
|     return ''; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const unique = () => { | ||||
|   const encountered = new Set(); | ||||
|   return (value: unknown): boolean => { | ||||
|     if (encountered.has(value)) return false; | ||||
|     encountered.add(value); | ||||
|     return true; | ||||
|   }; | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user