diff --git a/README.md b/README.md index 8012d696..f1e715c3 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ steps: - uses: actions/setup-node@v6 with: node-version: 24 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -221,6 +222,7 @@ jobs: uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -236,6 +238,7 @@ uses: actions/setup-node@v6 with: token: ${{ secrets.GH_DOTCOM_TOKEN }} node-version: 24 + package-manager-cache: false # Disable automatic npm caching if not required ``` 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/enterprise-server@3.2/admin/github-actions/managing-access-to-actions-from-githubcom/setting-up-the-tool-cache-on-self-hosted-runners-without-internet-access)" for more information. diff --git a/__tests__/cache-restore.test.ts b/__tests__/cache-restore.test.ts index 05142a88..c2c423fc 100644 --- a/__tests__/cache-restore.test.ts +++ b/__tests__/cache-restore.test.ts @@ -195,6 +195,7 @@ describe('cache-restore', () => { ])( 'restored dependencies for %s', async (packageManager, toolVersion, fileHash) => { + const expectedCacheKey = `node-cache-${platform}-${arch}-${packageManager}-${fileHash}`; setWorkspaceFor(packageManager as PackageManager); getExecOutputSpy.mockImplementation(async (command: any) => ({ stdout: command.includes('version') @@ -207,12 +208,20 @@ describe('cache-restore', () => { await restoreCache(packageManager, ''); expect(hashFilesSpy).toHaveBeenCalled(); expect(infoSpy).toHaveBeenCalledWith( - `Cache restored from key: node-cache-${platform}-${arch}-${packageManager}-${fileHash}` + `Cache restored from key: ${expectedCacheKey}` ); expect(infoSpy).not.toHaveBeenCalledWith( `${packageManager} cache is not found` ); expect(setOutputSpy).toHaveBeenCalledWith('cache-hit', true); + expect(setOutputSpy).toHaveBeenCalledWith( + 'cache-primary-key', + expectedCacheKey + ); + expect(setOutputSpy).toHaveBeenCalledWith( + 'cache-matched-key', + expectedCacheKey + ); } ); }); @@ -226,6 +235,7 @@ describe('cache-restore', () => { ])( 'dependencies are changed %s', async (packageManager, toolVersion, fileHash) => { + const expectedCacheKey = `node-cache-${platform}-${arch}-${packageManager}-${fileHash}`; setWorkspaceFor(packageManager as PackageManager); getExecOutputSpy.mockImplementation(async (command: any) => ({ stdout: command.includes('version') @@ -242,6 +252,14 @@ describe('cache-restore', () => { `${packageManager} cache is not found` ); expect(setOutputSpy).toHaveBeenCalledWith('cache-hit', false); + expect(setOutputSpy).toHaveBeenCalledWith( + 'cache-primary-key', + expectedCacheKey + ); + expect(setOutputSpy).toHaveBeenCalledWith( + 'cache-matched-key', + undefined + ); } ); }); diff --git a/action.yml b/action.yml index 79813721..dc1cefd5 100644 --- a/action.yml +++ b/action.yml @@ -34,6 +34,10 @@ inputs: outputs: cache-hit: description: 'A boolean value to indicate if a cache was hit.' + cache-primary-key: + description: 'The key used to restore and save the cache.' + cache-matched-key: + description: 'The key of the cache that was restored. Empty when no cache is found.' node-version: description: 'The installed node version.' runs: diff --git a/dist/setup/index.js b/dist/setup/index.js index c86c6131..57761756 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -98303,6 +98303,7 @@ const cache_restore_restoreCache = async (packageManager, cacheDependencyPath) = const primaryKey = `${keyPrefix}-${fileHash}`; core_debug(`primary key is ${primaryKey}`); saveState(State.CachePrimaryKey, primaryKey); + setOutput('cache-primary-key', primaryKey); const isManagedByYarnBerry = await repoHasYarnBerryManagedDependencies(packageManagerInfo, cacheDependencyPath); let cacheKey; if (isManagedByYarnBerry) { @@ -98313,6 +98314,8 @@ const cache_restore_restoreCache = async (packageManager, cacheDependencyPath) = cacheKey = await restoreCache(cachePaths, primaryKey); } setOutput('cache-hit', Boolean(cacheKey)); + setOutput('cache-matched-key', cacheKey); + core_debug(`cache-matched-key is ${cacheKey}`); if (!cacheKey) { core_info(`${packageManager} cache is not found`); return; diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 5f0edfb0..3b4417f1 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -69,6 +69,7 @@ steps: with: node-version: '24' check-latest: true + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -86,6 +87,7 @@ steps: - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -131,6 +133,7 @@ jobs: with: node-version: '24' architecture: 'x64' # optional, x64 or x86. If not specified, x64 will be used by default + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -151,6 +154,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24.0.0-v8-canary' # it will install the latest v8 canary release for node 24.0.0 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -166,6 +170,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24-v8-canary' # it will install the latest v8 canary release for node 24 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -182,6 +187,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: 'v24.0.0-v8-canary2025030537242e55ac' + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -202,6 +208,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24-nightly' # it will install the latest nightly release for node 24 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -218,6 +225,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24.0.0-nightly' # it will install the latest nightly release for node 24.0.0 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -234,6 +242,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24.0.0-nightly202505066102159fa1' + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -252,11 +261,12 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24.0.0-rc.4' + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` -**Note:** Unlike nightly versions, which support version range specifiers, you must specify the exact version for a release candidate: `24.0.0-rc.4`. +**Note**: Unlike nightly versions, which support version range specifiers, you must specify the exact version for a release candidate: `24.0.0-rc.4`. ## Caching packages data The action follows [actions/cache](https://github.com/actions/cache/blob/main/examples.md#node---npm) guidelines, and caches global cache on the machine instead of `node_modules`, so cache can be reused between different Node.js versions. @@ -344,6 +354,7 @@ steps: uses: actions/setup-node@v6 with: node-version: '24' + package-manager-cache: false # Disable automatic npm caching if not required - name: Normalize runner architecture shell: bash @@ -404,6 +415,7 @@ jobs: with: node-version: ${{ matrix.node_version }} architecture: ${{ matrix.architecture }} + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -416,6 +428,7 @@ steps: with: node-version: '24.x' registry-url: 'https://registry.npmjs.org' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: npm ci - run: npm publish env: @@ -423,6 +436,7 @@ steps: - uses: actions/setup-node@v6 with: registry-url: 'https://npm.pkg.github.com' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -436,6 +450,7 @@ steps: with: node-version: '24.x' registry-url: + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: yarn install --frozen-lockfile - run: yarn publish env: @@ -443,6 +458,7 @@ steps: - uses: actions/setup-node@v6 with: registry-url: 'https://npm.pkg.github.com' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: yarn publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -456,6 +472,7 @@ steps: with: node-version: '24.x' registry-url: 'https://registry.npmjs.org' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk # Skip post-install scripts here, as a malicious # script could steal NODE_AUTH_TOKEN. - run: npm ci --ignore-scripts @@ -475,6 +492,7 @@ steps: - uses: actions/setup-node@v6 with: node-version: '24.x' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - name: Setup .yarnrc.yml run: | yarn config set npmScopes.my-org.npmRegistryServer "https://npm.pkg.github.com" @@ -505,6 +523,8 @@ Trusted publishing requires a compatible npm version: You must also configure a **Trusted Publisher** in npm for your package/scope that matches your GitHub repository and workflow (and optional environment, if used). +> **Note**: In publishing workflows, set `package-manager-cache: false` because setup-node enables npm caching automatically when `package.json` specifies npm via `packageManager` or `devEngines.packageManager` (see [Running without a lockfile](#running-without-a-lockfile)), and a poisoned cache may expose credentials (including OIDC tokens) to attacker-controlled code. + ### Example workflow ```yaml @@ -519,6 +539,7 @@ You must also configure a **Trusted Publisher** in npm for your package/scope th with: node-version: '24' registry-url: 'https://registry.npmjs.org' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: npm ci - run: npm run build --if-present @@ -542,4 +563,5 @@ The token will be passed in the `Authorization` header. node-version: '24.x' mirror: 'https://nodejs.org/dist' mirror-token: 'your-mirror-token' + cache-package-manager: false # Disable automatic npm caching if not required ``` diff --git a/src/cache-restore.ts b/src/cache-restore.ts index a95b834e..45a8dbb9 100644 --- a/src/cache-restore.ts +++ b/src/cache-restore.ts @@ -45,6 +45,7 @@ export const restoreCache = async ( core.debug(`primary key is ${primaryKey}`); core.saveState(State.CachePrimaryKey, primaryKey); + core.setOutput('cache-primary-key', primaryKey); const isManagedByYarnBerry = await repoHasYarnBerryManagedDependencies( packageManagerInfo, @@ -61,6 +62,8 @@ export const restoreCache = async ( } core.setOutput('cache-hit', Boolean(cacheKey)); + core.setOutput('cache-matched-key', cacheKey); + core.debug(`cache-matched-key is ${cacheKey}`); if (!cacheKey) { core.info(`${packageManager} cache is not found`);