mirror of
				https://github.com/actions/setup-node.git
				synced 2025-10-31 23:23:35 +00:00 
			
		
		
		
	Enhance caching in setup-node with automatic package manager detection (#1348)
* setup node in local * Enhance caching in setup-node with package manager filed detection * updated with array * update the field
This commit is contained in:
		
							parent
							
								
									5e2628c959
								
							
						
					
					
						commit
						d7a11313b5
					
				
							
								
								
									
										25
									
								
								.github/workflows/e2e-cache.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/e2e-cache.yml
									
									
									
									
										vendored
									
									
								
							| @ -243,3 +243,28 @@ jobs: | |||||||
|           cache-dependency-path: | |           cache-dependency-path: | | ||||||
|             sub2/*.lock |             sub2/*.lock | ||||||
|             sub3/*.lock |             sub3/*.lock | ||||||
|  | 
 | ||||||
|  |   node-npm-package-manager-cache: | ||||||
|  |     name: Test enabling cache if package manager field is present (Node ${{ matrix.node-version }}, ${{ matrix.os }}) | ||||||
|  |     runs-on: ${{ matrix.os }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         os: [ubuntu-latest, windows-latest, macos-latest, macos-13] | ||||||
|  |         node-version: [18, 20, 22] | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v4 | ||||||
|  |       - name: Create package.json with packageManager field | ||||||
|  |         run: | | ||||||
|  |           echo '{ "name": "test-project", "version": "1.0.0", "packageManager": "npm@8.0.0" }' > package.json | ||||||
|  |       - name: Clean global cache | ||||||
|  |         run: npm cache clean --force | ||||||
|  |       - name: Setup Node with caching enabled | ||||||
|  |         uses: ./ | ||||||
|  |         with: | ||||||
|  |           node-version: ${{ matrix.node-version }} | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: npm install | ||||||
|  |       - name: Verify node and npm | ||||||
|  |         run: __tests__/verify-node.sh "${{ matrix.node-version }}" | ||||||
|  |         shell: bash | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							| @ -135,7 +135,19 @@ It's **always** recommended to commit the lockfile of your package manager for s | |||||||
| 
 | 
 | ||||||
| ## Caching global packages data | ## Caching global packages data | ||||||
| 
 | 
 | ||||||
| The action has a built-in functionality for caching and restoring dependencies. It uses [actions/cache](https://github.com/actions/cache) under the hood for caching global packages data but requires less configuration settings. Supported package managers are `npm`, `yarn`, `pnpm` (v6.10+). The `cache` input is optional, and caching is turned off by default. | The action has a built-in functionality for caching and restoring dependencies. It uses [actions/cache](https://github.com/actions/cache) under the hood for caching global packages data but requires less configuration settings. Supported package managers are `npm`, `yarn`, `pnpm` (v6.10+). The `cache` input is optional. | ||||||
|  | 
 | ||||||
|  | Caching is turned on by default when a `packageManager` field is detected in the `package.json` file. The `package-manager-cache` input provides control over this automatic caching behavior. By default, `package-manager-cache` is set to `true`, which enables caching when a valid package manager field is detected in the `package.json` file. To disable this automatic caching, set the `package-manager-cache` input to `false`. | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | steps: | ||||||
|  | - uses: actions/checkout@v4 | ||||||
|  | - uses: actions/setup-node@v4 | ||||||
|  |   with: | ||||||
|  |     package-manager-cache: false | ||||||
|  | - run: npm ci | ||||||
|  | ``` | ||||||
|  | > If no valid `packageManager` field is detected in the `package.json` file, caching will remain disabled unless explicitly configured. | ||||||
| 
 | 
 | ||||||
| The action defaults to search for the dependency file (`package-lock.json`, `npm-shrinkwrap.json` or `yarn.lock`) in the repository root, and uses its hash as a part of the cache key. Use `cache-dependency-path` for cases when multiple dependency files are used, or they are located in different subdirectories. | The action defaults to search for the dependency file (`package-lock.json`, `npm-shrinkwrap.json` or `yarn.lock`) in the repository root, and uses its hash as a part of the cache key. Use `cache-dependency-path` for cases when multiple dependency files are used, or they are located in different subdirectories. | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ describe('main tests', () => { | |||||||
| 
 | 
 | ||||||
|   let infoSpy: jest.SpyInstance; |   let infoSpy: jest.SpyInstance; | ||||||
|   let warningSpy: jest.SpyInstance; |   let warningSpy: jest.SpyInstance; | ||||||
|  |   let saveStateSpy: jest.SpyInstance; | ||||||
|   let inSpy: jest.SpyInstance; |   let inSpy: jest.SpyInstance; | ||||||
|   let setOutputSpy: jest.SpyInstance; |   let setOutputSpy: jest.SpyInstance; | ||||||
|   let startGroupSpy: jest.SpyInstance; |   let startGroupSpy: jest.SpyInstance; | ||||||
| @ -53,6 +54,8 @@ describe('main tests', () => { | |||||||
|     setOutputSpy.mockImplementation(() => {}); |     setOutputSpy.mockImplementation(() => {}); | ||||||
|     warningSpy = jest.spyOn(core, 'warning'); |     warningSpy = jest.spyOn(core, 'warning'); | ||||||
|     warningSpy.mockImplementation(() => {}); |     warningSpy.mockImplementation(() => {}); | ||||||
|  |     saveStateSpy = jest.spyOn(core, 'saveState'); | ||||||
|  |     saveStateSpy.mockImplementation(() => {}); | ||||||
|     startGroupSpy = jest.spyOn(core, 'startGroup'); |     startGroupSpy = jest.spyOn(core, 'startGroup'); | ||||||
|     startGroupSpy.mockImplementation(() => {}); |     startGroupSpy.mockImplementation(() => {}); | ||||||
|     endGroupSpy = jest.spyOn(core, 'endGroup'); |     endGroupSpy = jest.spyOn(core, 'endGroup'); | ||||||
| @ -280,4 +283,65 @@ describe('main tests', () => { | |||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   describe('cache feature tests', () => { | ||||||
|  |     it('Should enable caching with the resolved package manager from packageManager field in package.json when the cache input is not provided', async () => { | ||||||
|  |       inputs['package-manager-cache'] = 'true'; | ||||||
|  |       inputs['cache'] = ''; // No cache input is provided
 | ||||||
|  | 
 | ||||||
|  |       inSpy.mockImplementation(name => inputs[name]); | ||||||
|  | 
 | ||||||
|  |       const readFileSpy = jest.spyOn(fs, 'readFileSync'); | ||||||
|  |       readFileSpy.mockImplementation(() => | ||||||
|  |         JSON.stringify({ | ||||||
|  |           packageManager: 'yarn@3.2.0' | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       await main.run(); | ||||||
|  | 
 | ||||||
|  |       expect(saveStateSpy).toHaveBeenCalledWith(expect.anything(), 'yarn'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Should not enable caching if the packageManager field is missing in package.json and the cache input is not provided', async () => { | ||||||
|  |       inputs['package-manager-cache'] = 'true'; | ||||||
|  |       inputs['cache'] = ''; // No cache input is provided
 | ||||||
|  | 
 | ||||||
|  |       inSpy.mockImplementation(name => inputs[name]); | ||||||
|  | 
 | ||||||
|  |       const readFileSpy = jest.spyOn(fs, 'readFileSync'); | ||||||
|  |       readFileSpy.mockImplementation(() => | ||||||
|  |         JSON.stringify({ | ||||||
|  |           //packageManager field is not present
 | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       await main.run(); | ||||||
|  | 
 | ||||||
|  |       expect(saveStateSpy).not.toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Should skip caching when package-manager-cache is false', async () => { | ||||||
|  |       inputs['package-manager-cache'] = 'false'; | ||||||
|  |       inputs['cache'] = ''; // No cache input is provided
 | ||||||
|  | 
 | ||||||
|  |       inSpy.mockImplementation(name => inputs[name]); | ||||||
|  | 
 | ||||||
|  |       await main.run(); | ||||||
|  | 
 | ||||||
|  |       expect(saveStateSpy).not.toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Should enable caching with cache input explicitly provided', async () => { | ||||||
|  |       inputs['package-manager-cache'] = 'true'; | ||||||
|  |       inputs['cache'] = 'npm'; // Explicit cache input provided
 | ||||||
|  | 
 | ||||||
|  |       inSpy.mockImplementation(name => inputs[name]); | ||||||
|  |       isCacheActionAvailable.mockReturnValue(true); | ||||||
|  | 
 | ||||||
|  |       await main.run(); | ||||||
|  | 
 | ||||||
|  |       expect(saveStateSpy).toHaveBeenCalledWith(expect.anything(), 'npm'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -23,6 +23,9 @@ inputs: | |||||||
|     default: ${{ github.server_url == 'https://github.com' && github.token || '' }} |     default: ${{ github.server_url == 'https://github.com' && github.token || '' }} | ||||||
|   cache: |   cache: | ||||||
|     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.' | ||||||
|  |   package-manager-cache: | ||||||
|  |     description: 'Set to false to disable automatic caching based on the package manager field in package.json. By default, caching is enabled if the package manager field is present.' | ||||||
|  |     default: true | ||||||
|   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.' | ||||||
|   mirror: |   mirror: | ||||||
|  | |||||||
							
								
								
									
										30
									
								
								dist/setup/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								dist/setup/index.js
									
									
									
									
										vendored
									
									
								
							| @ -99583,9 +99583,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { | |||||||
|     return (mod && mod.__esModule) ? mod : { "default": mod }; |     return (mod && mod.__esModule) ? mod : { "default": mod }; | ||||||
| }; | }; | ||||||
| Object.defineProperty(exports, "__esModule", ({ value: true })); | Object.defineProperty(exports, "__esModule", ({ value: true })); | ||||||
| exports.run = void 0; | exports.getNameFromPackageManagerField = exports.run = void 0; | ||||||
| const core = __importStar(__nccwpck_require__(37484)); | const core = __importStar(__nccwpck_require__(37484)); | ||||||
| const os_1 = __importDefault(__nccwpck_require__(70857)); | const os_1 = __importDefault(__nccwpck_require__(70857)); | ||||||
|  | const fs_1 = __importDefault(__nccwpck_require__(79896)); | ||||||
| const auth = __importStar(__nccwpck_require__(98789)); | const auth = __importStar(__nccwpck_require__(98789)); | ||||||
| const path = __importStar(__nccwpck_require__(16928)); | const path = __importStar(__nccwpck_require__(16928)); | ||||||
| const cache_restore_1 = __nccwpck_require__(44326); | const cache_restore_1 = __nccwpck_require__(44326); | ||||||
| @ -99603,6 +99604,8 @@ function run() { | |||||||
|             const version = resolveVersionInput(); |             const version = resolveVersionInput(); | ||||||
|             let arch = core.getInput('architecture'); |             let arch = core.getInput('architecture'); | ||||||
|             const cache = core.getInput('cache'); |             const cache = core.getInput('cache'); | ||||||
|  |             const packagemanagercache = (core.getInput('package-manager-cache') || 'true').toUpperCase() === | ||||||
|  |                 'TRUE'; | ||||||
|             // if architecture supplied but node-version is not
 |             // if architecture supplied but node-version is not
 | ||||||
|             // if we don't throw a warning, the already installed x64 node will be used which is not probably what user meant.
 |             // if we don't throw a warning, the already installed x64 node will be used which is not probably what user meant.
 | ||||||
|             if (arch && !version) { |             if (arch && !version) { | ||||||
| @ -99636,11 +99639,16 @@ function run() { | |||||||
|             if (registryUrl) { |             if (registryUrl) { | ||||||
|                 auth.configAuthentication(registryUrl, alwaysAuth); |                 auth.configAuthentication(registryUrl, alwaysAuth); | ||||||
|             } |             } | ||||||
|  |             const resolvedPackageManager = getNameFromPackageManagerField(); | ||||||
|  |             const cacheDependencyPath = core.getInput('cache-dependency-path'); | ||||||
|             if (cache && (0, cache_utils_1.isCacheFeatureAvailable)()) { |             if (cache && (0, cache_utils_1.isCacheFeatureAvailable)()) { | ||||||
|                 core.saveState(constants_1.State.CachePackageManager, cache); |                 core.saveState(constants_1.State.CachePackageManager, cache); | ||||||
|                 const cacheDependencyPath = core.getInput('cache-dependency-path'); |  | ||||||
|                 yield (0, cache_restore_1.restoreCache)(cache, cacheDependencyPath); |                 yield (0, cache_restore_1.restoreCache)(cache, cacheDependencyPath); | ||||||
|             } |             } | ||||||
|  |             else if (resolvedPackageManager && packagemanagercache) { | ||||||
|  |                 core.saveState(constants_1.State.CachePackageManager, resolvedPackageManager); | ||||||
|  |                 yield (0, cache_restore_1.restoreCache)(resolvedPackageManager, cacheDependencyPath); | ||||||
|  |             } | ||||||
|             const matchersPath = path.join(__dirname, '../..', '.github'); |             const matchersPath = path.join(__dirname, '../..', '.github'); | ||||||
|             core.info(`##[add-matcher]${path.join(matchersPath, 'tsc.json')}`); |             core.info(`##[add-matcher]${path.join(matchersPath, 'tsc.json')}`); | ||||||
|             core.info(`##[add-matcher]${path.join(matchersPath, 'eslint-stylish.json')}`); |             core.info(`##[add-matcher]${path.join(matchersPath, 'eslint-stylish.json')}`); | ||||||
| @ -99674,6 +99682,24 @@ function resolveVersionInput() { | |||||||
|     } |     } | ||||||
|     return version; |     return version; | ||||||
| } | } | ||||||
|  | function getNameFromPackageManagerField() { | ||||||
|  |     // Check packageManager field in package.json
 | ||||||
|  |     const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm']; | ||||||
|  |     try { | ||||||
|  |         const packageJson = JSON.parse(fs_1.default.readFileSync(path.join(process.env.GITHUB_WORKSPACE, 'package.json'), 'utf-8')); | ||||||
|  |         const pm = packageJson.packageManager; | ||||||
|  |         if (typeof pm === 'string') { | ||||||
|  |             const regex = new RegExp(`^(?:\\^)?(${SUPPORTED_PACKAGE_MANAGERS.join('|')})@`); | ||||||
|  |             const match = pm.match(regex); | ||||||
|  |             return match ? match[1] : undefined; | ||||||
|  |         } | ||||||
|  |         return undefined; | ||||||
|  |     } | ||||||
|  |     catch (err) { | ||||||
|  |         return undefined; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | exports.getNameFromPackageManagerField = getNameFromPackageManagerField; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /***/ }), | /***/ }), | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ 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
 | ||||||
| // throw an uncaught exception.  Instead of failing this action, just warn.
 | // throw an uncaught exception.  Instead of failing this action, just warn.
 | ||||||
|  | 
 | ||||||
| process.on('uncaughtException', e => { | process.on('uncaughtException', e => { | ||||||
|   const warningPrefix = '[warning]'; |   const warningPrefix = '[warning]'; | ||||||
|   core.info(`${warningPrefix}${e.message}`); |   core.info(`${warningPrefix}${e.message}`); | ||||||
|  | |||||||
							
								
								
									
										34
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								src/main.ts
									
									
									
									
									
								
							| @ -1,6 +1,7 @@ | |||||||
| import * as core from '@actions/core'; | import * as core from '@actions/core'; | ||||||
| 
 | 
 | ||||||
| import os from 'os'; | import os from 'os'; | ||||||
|  | import fs from 'fs'; | ||||||
| 
 | 
 | ||||||
| import * as auth from './authutil'; | import * as auth from './authutil'; | ||||||
| import * as path from 'path'; | import * as path from 'path'; | ||||||
| @ -20,6 +21,9 @@ export async function run() { | |||||||
| 
 | 
 | ||||||
|     let arch = core.getInput('architecture'); |     let arch = core.getInput('architecture'); | ||||||
|     const cache = core.getInput('cache'); |     const cache = core.getInput('cache'); | ||||||
|  |     const packagemanagercache = | ||||||
|  |       (core.getInput('package-manager-cache') || 'true').toUpperCase() === | ||||||
|  |       'TRUE'; | ||||||
| 
 | 
 | ||||||
|     // if architecture supplied but node-version is not
 |     // if architecture supplied but node-version is not
 | ||||||
|     // if we don't throw a warning, the already installed x64 node will be used which is not probably what user meant.
 |     // if we don't throw a warning, the already installed x64 node will be used which is not probably what user meant.
 | ||||||
| @ -63,10 +67,14 @@ export async function run() { | |||||||
|       auth.configAuthentication(registryUrl, alwaysAuth); |       auth.configAuthentication(registryUrl, alwaysAuth); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const resolvedPackageManager = getNameFromPackageManagerField(); | ||||||
|  |     const cacheDependencyPath = core.getInput('cache-dependency-path'); | ||||||
|     if (cache && isCacheFeatureAvailable()) { |     if (cache && isCacheFeatureAvailable()) { | ||||||
|       core.saveState(State.CachePackageManager, cache); |       core.saveState(State.CachePackageManager, cache); | ||||||
|       const cacheDependencyPath = core.getInput('cache-dependency-path'); |  | ||||||
|       await restoreCache(cache, cacheDependencyPath); |       await restoreCache(cache, cacheDependencyPath); | ||||||
|  |     } else if (resolvedPackageManager && packagemanagercache) { | ||||||
|  |       core.saveState(State.CachePackageManager, resolvedPackageManager); | ||||||
|  |       await restoreCache(resolvedPackageManager, cacheDependencyPath); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const matchersPath = path.join(__dirname, '../..', '.github'); |     const matchersPath = path.join(__dirname, '../..', '.github'); | ||||||
| @ -117,3 +125,27 @@ function resolveVersionInput(): string { | |||||||
| 
 | 
 | ||||||
|   return version; |   return version; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function getNameFromPackageManagerField(): string | undefined { | ||||||
|  |   // Check packageManager field in package.json
 | ||||||
|  |   const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm']; | ||||||
|  |   try { | ||||||
|  |     const packageJson = JSON.parse( | ||||||
|  |       fs.readFileSync( | ||||||
|  |         path.join(process.env.GITHUB_WORKSPACE!, 'package.json'), | ||||||
|  |         'utf-8' | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |     const pm = packageJson.packageManager; | ||||||
|  |     if (typeof pm === 'string') { | ||||||
|  |       const regex = new RegExp( | ||||||
|  |         `^(?:\\^)?(${SUPPORTED_PACKAGE_MANAGERS.join('|')})@` | ||||||
|  |       ); | ||||||
|  |       const match = pm.match(regex); | ||||||
|  |       return match ? match[1] : undefined; | ||||||
|  |     } | ||||||
|  |     return undefined; | ||||||
|  |   } catch (err) { | ||||||
|  |     return undefined; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user