Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 33 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ This action provides the following functionality for GitHub Actions users:
- Registering problem matchers for error output
- Configuring authentication for GPR or npm

## Breaking changes in V6

- Caching is now automatically enabled for npm projects when the `packageManager` field in `package.json` is set to `npm`. For other package managers, such as Yarn and pnpm, caching is disabled by default and must be configured manually using the `cache` input.

## Breaking changes in V5

- Enabled caching by default with package manager detection if no cache input is provided.
Expand All @@ -28,7 +32,7 @@ See [action.yml](action.yml)

<!-- start usage -->
```yaml
- uses: actions/setup-node@v5
- uses: actions/setup-node@v6
with:
# Version Spec of the version to use in SemVer notation.
# It also admits such aliases as lts/*, latest, nightly and canary builds
Expand Down Expand Up @@ -67,7 +71,8 @@ See [action.yml](action.yml)
# Default: ''
cache: ''

# Used 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 and no cache input is provided'
# Controls automatic caching for npm. By default, caching for npm is enabled if the packageManager field in package.json specifies npm and no explicit cache input is provided.
# To disable automatic caching for npm, set package-manager-cache to false.
# default: true
package-manager-cache: true

Expand Down Expand Up @@ -113,9 +118,9 @@ See [action.yml](action.yml)
```yaml
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v6
with:
node-version: 18
node-version: 24
- run: npm ci
- run: npm test
```
Expand All @@ -132,8 +137,8 @@ The `node-version` input supports the Semantic Versioning Specification, for mor

Examples:

- Major versions: `18`, `20`
- More specific versions: `10.15`, `16.15.1` , `18.4.0`
- Major versions: `22`, `24`
- More specific versions: `20.19`, `22.17.1` , `24.8.0`
- NVM LTS syntax: `lts/erbium`, `lts/fermium`, `lts/*`, `lts/-n`
- Latest release: `*` or `latest`/`current`/`node`

Expand All @@ -151,18 +156,6 @@ It's **always** recommended to commit the lockfile of your package manager for s

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 and no `cache` input is provided. 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@v5
- uses: actions/setup-node@v5
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. For workflows with elevated privileges or access to sensitive information, we recommend disabling automatic caching by setting `package-manager-cache: false` when caching is not needed for secure operation.

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.

**Note:** The action does not cache `node_modules`
Expand All @@ -174,9 +167,9 @@ See the examples of using cache for `yarn`/`pnpm` and `cache-dependency-path` in
```yaml
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v6
with:
node-version: 20
node-version: 24
cache: 'npm'
- run: npm ci
- run: npm test
Expand All @@ -187,15 +180,29 @@ steps:
```yaml
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v6
with:
node-version: 20
node-version: 24
cache: 'npm'
cache-dependency-path: subdir/package-lock.json
- run: npm ci
- run: npm test
```

Caching for npm dependencies is automatically enabled when your `package.json` contains a `packageManager` field set to `npm` and no explicit cache input is provided.

This behavior is controlled by the `package-manager-cache` input, which defaults to `true`. To turn off automatic caching, set `package-manager-cache` to `false`.

```yaml
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
package-manager-cache: false
- run: npm ci
```
> If your `package.json` file does not include a `packageManager` field set to `npm`, caching will be disabled unless you explicitly enable it. For workflows with elevated privileges or access to sensitive information, we recommend disabling automatic caching for npm by setting `package-manager-cache: false` when caching is not required for secure operation.

## Matrix Testing

```yaml
Expand All @@ -204,12 +211,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 14, 16, 18 ]
node: [ 20, 22, 24 ]
name: Node ${{ matrix.node }} sample
steps:
- uses: actions/checkout@v5
- name: Setup node
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- run: npm ci
Expand All @@ -223,10 +230,10 @@ jobs:
To get a higher rate limit, you can [generate a personal access token on github.com](https://github.com/settings/tokens/new) and pass it as the `token` input for the action:

```yaml
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
token: ${{ secrets.GH_DOTCOM_TOKEN }}
node-version: 20
node-version: 24
```

If the runner is not able to access github.com, any Nodejs versions requested during a workflow run must come from the runner's tool cache. See "[Setting up the tool cache on self-hosted runners without internet access](https://docs.github.com/en/[email protected]/admin/github-actions/managing-access-to-actions-from-githubcom/setting-up-the-tool-cache-on-self-hosted-runners-without-internet-access)" for more information.
Expand Down
112 changes: 98 additions & 14 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,34 +285,72 @@ 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 () => {
it('Should enable caching when packageManager is npm and cache input is not provided', async () => {
inputs['package-manager-cache'] = 'true';
inputs['cache'] = ''; // No cache input is provided
inputs['cache'] = '';
isCacheActionAvailable.mockImplementation(() => true);

inSpy.mockImplementation(name => inputs[name]);
const readFileSpy = jest.spyOn(fs, 'readFileSync');
readFileSpy.mockImplementation(() =>
JSON.stringify({
packageManager: '[email protected]'
})
);

await main.run();

expect(saveStateSpy).toHaveBeenCalledWith(expect.anything(), 'npm');
});

it('Should enable caching when devEngines.packageManager.name is "npm" and cache input is not provided', async () => {
inputs['package-manager-cache'] = 'true';
inputs['cache'] = '';
isCacheActionAvailable.mockImplementation(() => true);

inSpy.mockImplementation(name => inputs[name]);
const readFileSpy = jest.spyOn(fs, 'readFileSync');
readFileSpy.mockImplementation(() =>
JSON.stringify({
packageManager: '[email protected]'
devEngines: {
packageManager: {name: 'npm'}
}
})
);

await main.run();

expect(saveStateSpy).toHaveBeenCalledWith(expect.anything(), 'yarn');
expect(saveStateSpy).toHaveBeenCalledWith(expect.anything(), 'npm');
});

it('Should not enable caching if the packageManager field is missing in package.json and the cache input is not provided', async () => {
it('Should enable caching when devEngines.packageManager is array and one entry has name "npm"', async () => {
inputs['package-manager-cache'] = 'true';
inputs['cache'] = ''; // No cache input is provided
inputs['cache'] = '';
isCacheActionAvailable.mockImplementation(() => true);

inSpy.mockImplementation(name => inputs[name]);
const readFileSpy = jest.spyOn(fs, 'readFileSync');
readFileSpy.mockImplementation(() =>
JSON.stringify({
devEngines: {
packageManager: [{name: 'pnpm'}, {name: 'npm'}]
}
})
);

await main.run();

expect(saveStateSpy).toHaveBeenCalledWith(expect.anything(), 'npm');
});

it('Should not enable caching if packageManager is "[email protected]" and cache input is not provided', async () => {
inputs['package-manager-cache'] = 'true';
inputs['cache'] = '';
inSpy.mockImplementation(name => inputs[name]);
const readFileSpy = jest.spyOn(fs, 'readFileSync');
readFileSpy.mockImplementation(() =>
JSON.stringify({
//packageManager field is not present
packageManager: '[email protected]'
})
);

Expand All @@ -321,26 +359,72 @@ describe('main tests', () => {
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

it('Should not enable caching if devEngines.packageManager.name is "pnpm"', async () => {
inputs['package-manager-cache'] = 'true';
inputs['cache'] = '';
inSpy.mockImplementation(name => inputs[name]);
const readFileSpy = jest.spyOn(fs, 'readFileSync');
readFileSpy.mockImplementation(() =>
JSON.stringify({
devEngines: {
packageManager: {name: 'pnpm'}
}
})
);

await main.run();

expect(saveStateSpy).not.toHaveBeenCalled();
});

it('Should enable caching with cache input explicitly provided', async () => {
it('Should not enable caching if devEngines.packageManager is array without "npm"', async () => {
inputs['package-manager-cache'] = 'true';
inputs['cache'] = 'npm'; // Explicit cache input provided
inputs['cache'] = '';
inSpy.mockImplementation(name => inputs[name]);
const readFileSpy = jest.spyOn(fs, 'readFileSync');
readFileSpy.mockImplementation(() =>
JSON.stringify({
devEngines: {
packageManager: [{name: 'pnpm'}, {name: 'yarn'}]
}
})
);

await main.run();

expect(saveStateSpy).not.toHaveBeenCalled();
});

it('Should not enable caching if packageManager field is missing in package.json and cache input is not provided', async () => {
inputs['package-manager-cache'] = 'true';
inputs['cache'] = '';
inSpy.mockImplementation(name => inputs[name]);
isCacheActionAvailable.mockReturnValue(true);
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'] = '';
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';
inSpy.mockImplementation(name => inputs[name]);
isCacheActionAvailable.mockImplementation(() => true);
await main.run();
expect(saveStateSpy).toHaveBeenCalledWith(expect.anything(), 'npm');
});
});
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ inputs:
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.'
description: 'Set to false to disable automatic caching. By default, caching is enabled when npm is the specified package manager in package.json.'
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.'
Expand Down
50 changes: 31 additions & 19 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -99786,18 +99786,23 @@ 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);
yield (0, cache_restore_1.restoreCache)(cache, cacheDependencyPath);
}
else if (resolvedPackageManager && packagemanagercache) {
core.info("Detected package manager from package.json's packageManager field: " +
resolvedPackageManager +
'. Auto caching has been enabled for it. If you want to disable it, set package-manager-cache input to false');
core.saveState(constants_1.State.CachePackageManager, resolvedPackageManager);
yield (0, cache_restore_1.restoreCache)(resolvedPackageManager, cacheDependencyPath);
if ((0, cache_utils_1.isCacheFeatureAvailable)()) {
// if the cache input is provided, use it for caching.
if (cache) {
core.saveState(constants_1.State.CachePackageManager, cache);
yield (0, cache_restore_1.restoreCache)(cache, cacheDependencyPath);
// package manager npm is detected from package.json, enable auto-caching for npm.
}
else if (packagemanagercache) {
const resolvedPackageManager = getNameFromPackageManagerField();
if (resolvedPackageManager) {
core.info("Detected npm as the package manager from package.json's packageManager field. " +
'Auto caching has been enabled for npm. If you want to disable it, set package-manager-cache input to false');
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')}`);
Expand Down Expand Up @@ -99833,19 +99838,26 @@ function resolveVersionInput() {
return version;
}
function getNameFromPackageManagerField() {
// Check packageManager field in package.json
const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm'];
var _a;
const npmRegex = /^(\^)?npm(@.*)?$/; // matches "npm", "npm@...", "^npm@..."
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;
// Check devEngines.packageManager first (object or array)
const devPM = (_a = packageJson === null || packageJson === void 0 ? void 0 : packageJson.devEngines) === null || _a === void 0 ? void 0 : _a.packageManager;
const devPMArray = devPM ? (Array.isArray(devPM) ? devPM : [devPM]) : [];
for (const obj of devPMArray) {
if (typeof (obj === null || obj === void 0 ? void 0 : obj.name) === 'string' && npmRegex.test(obj.name)) {
return 'npm';
}
}
// Check top-level packageManager
const topLevelPM = packageJson === null || packageJson === void 0 ? void 0 : packageJson.packageManager;
if (typeof topLevelPM === 'string' && npmRegex.test(topLevelPM)) {
return 'npm';
}
return undefined;
}
catch (err) {
catch (_b) {
return undefined;
}
}
Expand Down
Loading
Loading