From d7a11313b581b306c961b506cfc8971208bb03f6 Mon Sep 17 00:00:00 2001 From: priya-kinthali <147703874+priya-kinthali@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:10:12 +0530 Subject: [PATCH] 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 --- .github/workflows/e2e-cache.yml | 25 +++++++++++++ README.md | 14 +++++++- __tests__/main.test.ts | 64 +++++++++++++++++++++++++++++++++ action.yml | 3 ++ dist/setup/index.js | 30 ++++++++++++++-- src/cache-save.ts | 1 + src/main.ts | 34 +++++++++++++++++- 7 files changed, 167 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-cache.yml b/.github/workflows/e2e-cache.yml index cede9b63..3cbab61e 100644 --- a/.github/workflows/e2e-cache.yml +++ b/.github/workflows/e2e-cache.yml @@ -243,3 +243,28 @@ jobs: cache-dependency-path: | sub2/*.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 diff --git a/README.md b/README.md index 92804e94..1ccefa7f 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,19 @@ It's **always** recommended to commit the lockfile of your package manager for s ## 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. diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 501741a6..5af57092 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -20,6 +20,7 @@ describe('main tests', () => { let infoSpy: jest.SpyInstance; let warningSpy: jest.SpyInstance; + let saveStateSpy: jest.SpyInstance; let inSpy: jest.SpyInstance; let setOutputSpy: jest.SpyInstance; let startGroupSpy: jest.SpyInstance; @@ -53,6 +54,8 @@ describe('main tests', () => { setOutputSpy.mockImplementation(() => {}); warningSpy = jest.spyOn(core, 'warning'); warningSpy.mockImplementation(() => {}); + saveStateSpy = jest.spyOn(core, 'saveState'); + saveStateSpy.mockImplementation(() => {}); startGroupSpy = jest.spyOn(core, 'startGroup'); startGroupSpy.mockImplementation(() => {}); 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'); + }); + }); }); diff --git a/action.yml b/action.yml index ef58e699..3fc2e0c2 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,9 @@ inputs: default: ${{ github.server_url == 'https://github.com' && github.token || '' }} cache: 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: 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: diff --git a/dist/setup/index.js b/dist/setup/index.js index 390170cc..c6381da6 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -99583,9 +99583,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.run = void 0; +exports.getNameFromPackageManagerField = exports.run = void 0; const core = __importStar(__nccwpck_require__(37484)); const os_1 = __importDefault(__nccwpck_require__(70857)); +const fs_1 = __importDefault(__nccwpck_require__(79896)); const auth = __importStar(__nccwpck_require__(98789)); const path = __importStar(__nccwpck_require__(16928)); const cache_restore_1 = __nccwpck_require__(44326); @@ -99603,6 +99604,8 @@ function run() { const version = resolveVersionInput(); let arch = core.getInput('architecture'); const cache = core.getInput('cache'); + const packagemanagercache = (core.getInput('package-manager-cache') || 'true').toUpperCase() === + 'TRUE'; // 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 (arch && !version) { @@ -99636,11 +99639,16 @@ function run() { if (registryUrl) { auth.configAuthentication(registryUrl, alwaysAuth); } + const resolvedPackageManager = getNameFromPackageManagerField(); + const cacheDependencyPath = core.getInput('cache-dependency-path'); if (cache && (0, cache_utils_1.isCacheFeatureAvailable)()) { core.saveState(constants_1.State.CachePackageManager, cache); - const cacheDependencyPath = core.getInput('cache-dependency-path'); 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'); core.info(`##[add-matcher]${path.join(matchersPath, 'tsc.json')}`); core.info(`##[add-matcher]${path.join(matchersPath, 'eslint-stylish.json')}`); @@ -99674,6 +99682,24 @@ function resolveVersionInput() { } 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; /***/ }), diff --git a/src/cache-save.ts b/src/cache-save.ts index 2522127a..bbfd2de6 100644 --- a/src/cache-save.ts +++ b/src/cache-save.ts @@ -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 // @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. + process.on('uncaughtException', e => { const warningPrefix = '[warning]'; core.info(`${warningPrefix}${e.message}`); diff --git a/src/main.ts b/src/main.ts index c36d8ec5..f169cef0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ import * as core from '@actions/core'; import os from 'os'; +import fs from 'fs'; import * as auth from './authutil'; import * as path from 'path'; @@ -20,6 +21,9 @@ export async function run() { let arch = core.getInput('architecture'); const cache = core.getInput('cache'); + const packagemanagercache = + (core.getInput('package-manager-cache') || 'true').toUpperCase() === + 'TRUE'; // 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. @@ -63,10 +67,14 @@ export async function run() { auth.configAuthentication(registryUrl, alwaysAuth); } + const resolvedPackageManager = getNameFromPackageManagerField(); + const cacheDependencyPath = core.getInput('cache-dependency-path'); if (cache && isCacheFeatureAvailable()) { core.saveState(State.CachePackageManager, cache); - const cacheDependencyPath = core.getInput('cache-dependency-path'); await restoreCache(cache, cacheDependencyPath); + } else if (resolvedPackageManager && packagemanagercache) { + core.saveState(State.CachePackageManager, resolvedPackageManager); + await restoreCache(resolvedPackageManager, cacheDependencyPath); } const matchersPath = path.join(__dirname, '../..', '.github'); @@ -117,3 +125,27 @@ function resolveVersionInput(): string { 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; + } +}