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 |       - 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 | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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` | ||||||
|   | |||||||
| @@ -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.` | ||||||
|       ); |       ); | ||||||
|   | |||||||
| @@ -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` | ||||||
|  |         ]); | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										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.' |     description: 'Used to specify a package manager for caching in the default directory. Supported values: npm, yarn, pnpm.' | ||||||
|   cache-dependency-path: |   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.' |     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 | #       escape valve for someone having issues or needing the absolute latest which isn't cached yet | ||||||
| outputs: | outputs: | ||||||
|   cache-hit:  |   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 {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)); | ||||||
|   | |||||||
| @@ -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; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user