Skip to content

chore(deps): update actions/setup-node action to v4.4.0 - autoclosed #1058

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

renovate[bot]
Copy link
Contributor

@renovate renovate bot commented Apr 15, 2025

This PR contains the following updates:

Package Type Update Change
actions/setup-node action minor v4.3.0 -> v4.4.0

Release Notes

actions/setup-node (actions/setup-node)

v4.4.0

Compare Source


Configuration

📅 Schedule: Branch creation - "* * * * 2-4" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Enabled.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate bot added dependencies Pull requests that update a dependency file renovate labels Apr 15, 2025
@renovate renovate bot enabled auto-merge (squash) April 15, 2025 02:47
Copy link
Contributor

[puLL-Merge] - actions/[email protected]

Diff
diff --git .github/eslint-compact.json .github/eslint-compact.json
index 72173f0c0..d577d4989 100644
--- .github/eslint-compact.json
+++ .github/eslint-compact.json
@@ -4,7 +4,7 @@
             "owner": "eslint-compact",
             "pattern": [
                 {
-                    "regexp": "^(.+):\\sline\\s(\\d+),\\scol\\s(\\d+),\\s(Error|Warning|Info)\\s-\\s(.+)\\s\\((.+)\\)$",
+                    "regexp": "^(.+):\\sline\\s(\\d+),\\scol\\s(\\d+),\\s([Ee]rror|[Ww]arning|[Ii]nfo)\\s-\\s(.+)\\s\\((.+)\\)$",
                     "file": 1,
                     "line": 2,
                     "column": 3,
diff --git .github/eslint-stylish.json .github/eslint-stylish.json
index 1d75d6c30..db9655b62 100644
--- .github/eslint-stylish.json
+++ .github/eslint-stylish.json
@@ -4,7 +4,7 @@
             "owner": "eslint-stylish",
             "pattern": [
                 {
-                    "regexp": "^([^\\s].*)$",
+                    "regexp": "^\\s*([^\\s].*)$",
                     "file": 1
                 },
                 {
diff --git .licenses/npm/@actions/cache.dep.yml .licenses/npm/@actions/cache.dep.yml
index 6e21be6a2..f70b140d2 100644
--- .licenses/npm/@actions/cache.dep.yml
+++ .licenses/npm/@actions/cache.dep.yml
@@ -1,6 +1,6 @@
 ---
 name: "@actions/cache"
-version: 4.0.2
+version: 4.0.3
 type: npm
 summary: Actions cache lib
 homepage: https://github.com/actions/toolkit/tree/main/packages/cache
diff --git .licenses/npm/@octokit/auth-token.dep.yml .licenses/npm/@octokit/auth-token.dep.yml
index 1ffe43e99..71f6dbf76 100644
--- .licenses/npm/@octokit/auth-token.dep.yml
+++ .licenses/npm/@octokit/auth-token.dep.yml
@@ -31,4 +31,4 @@ licenses:
     THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/core.dep.yml .licenses/npm/@octokit/core.dep.yml
index d6657a245..77713295c 100644
--- .licenses/npm/@octokit/core.dep.yml
+++ .licenses/npm/@octokit/core.dep.yml
@@ -31,4 +31,4 @@ licenses:
     THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/endpoint.dep.yml .licenses/npm/@octokit/endpoint.dep.yml
index 74b7649ec..748a61c57 100644
--- .licenses/npm/@octokit/endpoint.dep.yml
+++ .licenses/npm/@octokit/endpoint.dep.yml
@@ -31,4 +31,4 @@ licenses:
     THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/graphql.dep.yml .licenses/npm/@octokit/graphql.dep.yml
index c4faad910..268991d01 100644
--- .licenses/npm/@octokit/graphql.dep.yml
+++ .licenses/npm/@octokit/graphql.dep.yml
@@ -31,4 +31,4 @@ licenses:
     THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/openapi-types.dep.yml .licenses/npm/@octokit/openapi-types.dep.yml
index f2891938f..798f9e617 100644
--- .licenses/npm/@octokit/openapi-types.dep.yml
+++ .licenses/npm/@octokit/openapi-types.dep.yml
@@ -17,4 +17,4 @@ licenses:
     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/plugin-paginate-rest.dep.yml .licenses/npm/@octokit/plugin-paginate-rest.dep.yml
index 9f8cf25b9..f8090226f 100644
--- .licenses/npm/@octokit/plugin-paginate-rest.dep.yml
+++ .licenses/npm/@octokit/plugin-paginate-rest.dep.yml
@@ -17,4 +17,4 @@ licenses:
     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/plugin-rest-endpoint-methods.dep.yml .licenses/npm/@octokit/plugin-rest-endpoint-methods.dep.yml
index 922034848..02cb6c4ca 100644
--- .licenses/npm/@octokit/plugin-rest-endpoint-methods.dep.yml
+++ .licenses/npm/@octokit/plugin-rest-endpoint-methods.dep.yml
@@ -17,4 +17,4 @@ licenses:
     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/request-error.dep.yml .licenses/npm/@octokit/request-error.dep.yml
index 41a28b4e5..1141bf42d 100644
--- .licenses/npm/@octokit/request-error.dep.yml
+++ .licenses/npm/@octokit/request-error.dep.yml
@@ -31,4 +31,4 @@ licenses:
     THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/request.dep.yml .licenses/npm/@octokit/request.dep.yml
index 73a8abce3..179ed420f 100644
--- .licenses/npm/@octokit/request.dep.yml
+++ .licenses/npm/@octokit/request.dep.yml
@@ -32,4 +32,4 @@ licenses:
     THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/@octokit/types.dep.yml .licenses/npm/@octokit/types.dep.yml
index d13289b29..d146cd916 100644
--- .licenses/npm/@octokit/types.dep.yml
+++ .licenses/npm/@octokit/types.dep.yml
@@ -17,4 +17,4 @@ licenses:
     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 - sources: README.md
   text: "[MIT](LICENSE)"
-notices: []
+notices: []
\ No newline at end of file
diff --git .licenses/npm/universal-user-agent.dep.yml .licenses/npm/universal-user-agent.dep.yml
index 9cf02a9d5..c07307b84 100644
--- .licenses/npm/universal-user-agent.dep.yml
+++ .licenses/npm/universal-user-agent.dep.yml
@@ -1,9 +1,9 @@
 ---
 name: universal-user-agent
-version: 6.0.0
+version: 6.0.1
 type: npm
 summary: Get a user agent string in both browser and node
-homepage: 
+homepage:
 license: isc
 licenses:
 - sources: LICENSE.md
diff --git README.md README.md
index 0c5548981..92804e94e 100644
--- README.md
+++ README.md
@@ -76,6 +76,21 @@ See [action.yml](action.yml)
     # Set always-auth option in npmrc file.
     # Default: ''
     always-auth: ''
+
+    # Optional mirror to download binaries from.
+    # Artifacts need to match the official Node.js
+    # Example:
+    # V8 Canaray Build: <mirror_url>/download/v8-canary
+    # RC Build: <mirror_url>/download/rc
+    # Official: Build <mirror_url>/dist
+    # Nightly build: <mirror_url>/download/nightly
+    # Default: ''
+    mirror: ''
+
+    # Optional mirror token.
+    # The token will be used as a bearer token in the Authorization header
+    # Default: ''
+    mirror-token: ''
 ```
 <!-- end usage -->
 
diff --git __tests__/canary-installer.test.ts __tests__/canary-installer.test.ts
index 6d141fc3c..4393d7ef2 100644
--- __tests__/canary-installer.test.ts
+++ __tests__/canary-installer.test.ts
@@ -498,6 +498,70 @@ describe('setup-node', () => {
         );
       }
     );
+
+    it.each([
+      [
+        '20.0.0-v8-canary',
+        '20.0.0-v8-canary20221103f7e2421e91',
+        '20.0.0-v8-canary20221030fefe1c0879',
+        'https://my_mirror.org/download/v8-canary/v20.0.0-v8-canary20221103f7e2421e91/node-v20.0.0-v8-canary20221103f7e2421e91-linux-x64.tar.gz'
+      ],
+      [
+        '20-v8-canary',
+        '20.0.0-v8-canary20221103f7e2421e91',
+        '20.0.0-v8-canary20221030fefe1c0879',
+        'https://my_mirror.org/download/v8-canary/v20.0.0-v8-canary20221103f7e2421e91/node-v20.0.0-v8-canary20221103f7e2421e91-linux-x64.tar.gz'
+      ],
+      [
+        '19.0.0-v8-canary',
+        '19.0.0-v8-canary202210187d6960f23f',
+        '19.0.0-v8-canary202210172ec229fc56',
+        'https://my_mirror.org/download/v8-canary/v19.0.0-v8-canary202210187d6960f23f/node-v19.0.0-v8-canary202210187d6960f23f-linux-x64.tar.gz'
+      ],
+      [
+        '19-v8-canary',
+        '19.0.0-v8-canary202210187d6960f23f',
+        '19.0.0-v8-canary202210172ec229fc56',
+        'https://my_mirror.org/download/v8-canary/v19.0.0-v8-canary202210187d6960f23f/node-v19.0.0-v8-canary202210187d6960f23f-linux-x64.tar.gz'
+      ]
+    ])(
+      'get %s version from dist if check-latest is true',
+      async (input, expectedVersion, foundVersion, expectedUrl) => {
+        const foundToolPath = path.normalize(`/cache/node/${foundVersion}/x64`);
+        const toolPath = path.normalize(`/cache/node/${expectedVersion}/x64`);
+
+        inputs['node-version'] = input;
+        inputs['check-latest'] = 'true';
+        os['arch'] = 'x64';
+        os['platform'] = 'linux';
+        inputs['mirror'] = 'https://my_mirror.org';
+        inputs['mirror-token'] = 'faketoken';
+
+        findSpy.mockReturnValue(foundToolPath);
+        findAllVersionsSpy.mockReturnValue([
+          '20.0.0-v8-canary20221030fefe1c0879',
+          '19.0.0-v8-canary202210172ec229fc56',
+          '20.0.0-v8-canary2022102310ff1e5a8d'
+        ]);
+        dlSpy.mockImplementation(async () => '/some/temp/path');
+        exSpy.mockImplementation(async () => '/some/other/temp/path');
+        cacheSpy.mockImplementation(async () => toolPath);
+
+        // act
+        await main.run();
+
+        // assert
+        expect(findAllVersionsSpy).toHaveBeenCalled();
+        expect(logSpy).toHaveBeenCalledWith(
+          `Acquiring ${expectedVersion} - ${os.arch} from ${expectedUrl}`
+        );
+        expect(logSpy).toHaveBeenCalledWith('Extracting ...');
+        expect(logSpy).toHaveBeenCalledWith('Adding to the cache ...');
+        expect(cnSpy).toHaveBeenCalledWith(
+          `::add-path::${path.join(toolPath, 'bin')}${osm.EOL}`
+        );
+      }
+    );
   });
 
   describe('setup-node v8 canary tests', () => {
diff --git __tests__/nightly-installer.test.ts __tests__/nightly-installer.test.ts
index 87c437957..eece2c344 100644
--- __tests__/nightly-installer.test.ts
+++ __tests__/nightly-installer.test.ts
@@ -315,7 +315,7 @@ describe('setup-node', () => {
     await main.run();
 
     workingUrls.forEach(url => {
-      expect(dlSpy).toHaveBeenCalledWith(url);
+      expect(dlSpy).toHaveBeenCalledWith(url, undefined, undefined);
     });
     expect(cnSpy).toHaveBeenCalledWith(`::add-path::${toolPath}${osm.EOL}`);
   });
@@ -449,6 +449,54 @@ describe('setup-node', () => {
     }
   }, 100000);
 
+  it('acquires specified architecture of node from mirror', async () => {
+    for (const {arch, version, osSpec} of [
+      {
+        arch: 'x86',
+        version: '18.0.0-nightly202110204cb3e06ed8',
+        osSpec: 'win32'
+      },
+      {
+        arch: 'x86',
+        version: '20.0.0-nightly2022101987cdf7d412',
+        osSpec: 'win32'
+      }
+    ]) {
+      os.platform = osSpec;
+      os.arch = arch;
+      const fileExtension = os.platform === 'win32' ? '7z' : 'tar.gz';
+      const platform = {
+        linux: 'linux',
+        darwin: 'darwin',
+        win32: 'win'
+      }[os.platform];
+
+      inputs['node-version'] = version;
+      inputs['architecture'] = arch;
+      inputs['always-auth'] = false;
+      inputs['token'] = 'faketoken';
+      inputs['mirror'] = 'https://my-mirror.org';
+      inputs['mirror-token'] = 'my-mirror-token';
+
+      const expectedUrl = `https://my-mirror.org/download/nightly/v${version}/node-v${version}-${platform}-${arch}.${fileExtension}`;
+
+      // ... but not in the local cache
+      findSpy.mockImplementation(() => '');
+      findAllVersionsSpy.mockImplementation(() => []);
+
+      dlSpy.mockImplementation(async () => '/some/temp/path');
+      const toolPath = path.normalize(`/cache/node/${version}/${arch}`);
+      exSpy.mockImplementation(async () => '/some/other/temp/path');
+      cacheSpy.mockImplementation(async () => toolPath);
+
+      await main.run();
+      expect(dlSpy).toHaveBeenCalled();
+      expect(logSpy).toHaveBeenCalledWith(
+        `Acquiring ${version} - ${arch} from ${expectedUrl}`
+      );
+    }
+  }, 100000);
+
   describe('nightly versions', () => {
     it.each([
       [
diff --git __tests__/official-installer.test.ts __tests__/official-installer.test.ts
index 2d8f17cfa..f23183d33 100644
--- __tests__/official-installer.test.ts
+++ __tests__/official-installer.test.ts
@@ -282,6 +282,43 @@ describe('setup-node', () => {
     expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`);
   });
 
+  it('falls back to a version from node dist from mirror', async () => {
+    os.platform = 'linux';
+    os.arch = 'x64';
+
+    // a version which is not in the manifest but is in node dist
+    const versionSpec = '11.15.0';
+    const mirror = 'https://my_mirror_url';
+    inputs['node-version'] = versionSpec;
+    inputs['always-auth'] = false;
+    inputs['token'] = 'faketoken';
+    inputs['mirror'] = mirror;
+    inputs['mirror-token'] = 'faketoken';
+
+    // ... but not in the local cache
+    findSpy.mockImplementation(() => '');
+
+    dlSpy.mockImplementation(async () => '/some/temp/path');
+    const toolPath = path.normalize('/cache/node/11.15.0/x64');
+    exSpy.mockImplementation(async () => '/some/other/temp/path');
+    cacheSpy.mockImplementation(async () => toolPath);
+
+    await main.run();
+
+    const expPath = path.join(toolPath, 'bin');
+
+    expect(getManifestSpy).toHaveBeenCalled();
+    expect(logSpy).toHaveBeenCalledWith(
+      `Attempting to download ${versionSpec}...`
+    );
+    expect(logSpy).toHaveBeenCalledWith(
+      `Not found in manifest. Falling back to download directly from ${mirror}`
+    );
+    expect(dlSpy).toHaveBeenCalled();
+    expect(exSpy).toHaveBeenCalled();
+    expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`);
+  });
+
   it('falls back to a version from node dist', async () => {
     os.platform = 'linux';
     os.arch = 'x64';
@@ -828,4 +865,46 @@ describe('setup-node', () => {
       }
     );
   });
+
+  it('acquires specified architecture of node from mirror', async () => {
+    for (const {arch, version, osSpec} of [
+      {arch: 'x86', version: '12.16.2', osSpec: 'win32'},
+      {arch: 'x86', version: '14.0.0', osSpec: 'win32'}
+    ]) {
+      os.platform = osSpec;
+      os.arch = arch;
+      const fileExtension = os.platform === 'win32' ? '7z' : 'tar.gz';
+      const platform = {
+        linux: 'linux',
+        darwin: 'darwin',
+        win32: 'win'
+      }[os.platform];
+
+      inputs['node-version'] = version;
+      inputs['architecture'] = arch;
+      inputs['always-auth'] = false;
+      inputs['token'] = 'faketoken';
+      inputs['mirror'] = 'https://my_mirror_url';
+      inputs['mirror-token'] = 'faketoken';
+
+      const expectedUrl =
+        arch === 'x64'
+          ? `https://github.com/actions/node-versions/releases/download/${version}/node-${version}-${platform}-${arch}.zip`
+          : `https://my_mirror_url/dist/v${version}/node-v${version}-${platform}-${arch}.${fileExtension}`;
+
+      // ... but not in the local cache
+      findSpy.mockImplementation(() => '');
+
+      dlSpy.mockImplementation(async () => '/some/temp/path');
+      const toolPath = path.normalize(`/cache/node/${version}/${arch}`);
+      exSpy.mockImplementation(async () => '/some/other/temp/path');
+      cacheSpy.mockImplementation(async () => toolPath);
+
+      await main.run();
+      expect(dlSpy).toHaveBeenCalled();
+      expect(logSpy).toHaveBeenCalledWith(
+        `Acquiring ${version} - ${arch} from ${expectedUrl}`
+      );
+    }
+  }, 100000);
 });
diff --git action.yml action.yml
index 99db5869f..ef58e6991 100644
--- action.yml
+++ action.yml
@@ -25,6 +25,10 @@ inputs:
     description: 'Used to specify a package manager for caching in the default directory. Supported values: npm, yarn, pnpm.'
   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:
+    description: 'Used to specify an alternative mirror to downlooad Node.js binaries from'
+  mirror-token:
+    description: 'The token used as Authorization header when fetching from the mirror'
 # 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
 outputs:
diff --git dist/cache-save/index.js dist/cache-save/index.js
index 51fadc3fe..b12e8abb8 100644
--- dist/cache-save/index.js
+++ dist/cache-save/index.js
@@ -220,7 +220,7 @@ function restoreCacheV2(paths, primaryKey, restoreKeys, options, enableCrossOsAr
             };
             const response = yield twirpClient.GetCacheEntryDownloadURL(request);
             if (!response.ok) {
-                core.debug(`Cache not found for keys: ${keys.join(', ')}`);
+                core.debug(`Cache not found for version ${request.version} of keys: ${keys.join(', ')}`);
                 return undefined;
             }
             core.info(`Cache hit for: ${request.key}`);
@@ -2204,6 +2204,7 @@ const cacheUtils_1 = __nccwpck_require__(680);
 const auth_1 = __nccwpck_require__(4552);
 const http_client_1 = __nccwpck_require__(4844);
 const cache_twirp_client_1 = __nccwpck_require__(1486);
+const util_1 = __nccwpck_require__(7564);
 /**
  * This class is a wrapper around the CacheServiceClientJSON class generated by Twirp.
  *
@@ -2263,6 +2264,7 @@ class CacheServiceClient {
                     (0, core_1.debug)(`[Response] - ${response.message.statusCode}`);
                     (0, core_1.debug)(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`);
                     const body = JSON.parse(rawBody);
+                    (0, util_1.maskSecretUrls)(body);
                     (0, core_1.debug)(`Body: ${JSON.stringify(body, null, 2)}`);
                     if (this.isSuccessStatusCode(statusCode)) {
                         return { response, body };
@@ -2444,6 +2446,87 @@ exports.getUserAgentString = getUserAgentString;
 
 /***/ }),
 
+/***/ 7564:
+/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.maskSecretUrls = exports.maskSigUrl = void 0;
+const core_1 = __nccwpck_require__(7484);
+/**
+ * Masks the `sig` parameter in a URL and sets it as a secret.
+ *
+ * @param url - The URL containing the signature parameter to mask
+ * @remarks
+ * This function attempts to parse the provided URL and identify the 'sig' query parameter.
+ * If found, it registers both the raw and URL-encoded signature values as secrets using
+ * the Actions `setSecret` API, which prevents them from being displayed in logs.
+ *
+ * The function handles errors gracefully if URL parsing fails, logging them as debug messages.
+ *
+ * @example
+ * ```typescript
+ * // Mask a signature in an Azure SAS token URL
+ * maskSigUrl('https://example.blob.core.windows.net/container/file.txt?sig=abc123&se=2023-01-01');
+ * ```
+ */
+function maskSigUrl(url) {
+    if (!url)
+        return;
+    try {
+        const parsedUrl = new URL(url);
+        const signature = parsedUrl.searchParams.get('sig');
+        if (signature) {
+            (0, core_1.setSecret)(signature);
+            (0, core_1.setSecret)(encodeURIComponent(signature));
+        }
+    }
+    catch (error) {
+        (0, core_1.debug)(`Failed to parse URL: ${url} ${error instanceof Error ? error.message : String(error)}`);
+    }
+}
+exports.maskSigUrl = maskSigUrl;
+/**
+ * Masks sensitive information in URLs containing signature parameters.
+ * Currently supports masking 'sig' parameters in the 'signed_upload_url'
+ * and 'signed_download_url' properties of the provided object.
+ *
+ * @param body - The object should contain a signature
+ * @remarks
+ * This function extracts URLs from the object properties and calls maskSigUrl
+ * on each one to redact sensitive signature information. The function doesn't
+ * modify the original object; it only marks the signatures as secrets for
+ * logging purposes.
+ *
+ * @example
+ * ```typescript
+ * const responseBody = {
+ *   signed_upload_url: 'https://blob.core.windows.net/?sig=abc123',
+ *   signed_download_url: 'https://blob.core/windows.net/?sig=def456'
+ * };
+ * maskSecretUrls(responseBody);
+ * ```
+ */
+function maskSecretUrls(body) {
+    if (typeof body !== 'object' || body === null) {
+        (0, core_1.debug)('body is not an object or is null');
+        return;
+    }
+    if ('signed_upload_url' in body &&
+        typeof body.signed_upload_url === 'string') {
+        maskSigUrl(body.signed_upload_url);
+    }
+    if ('signed_download_url' in body &&
+        typeof body.signed_download_url === 'string') {
+        maskSigUrl(body.signed_download_url);
+    }
+}
+exports.maskSecretUrls = maskSecretUrls;
+//# sourceMappingURL=util.js.map
+
+/***/ }),
+
 /***/ 5321:
 /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
 
@@ -90234,7 +90317,7 @@ module.exports = parseParams
 /***/ ((module) => {
 
 "use strict";
-module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"4.0.2","preview":true,"description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","main":"lib/cache.js","types":"lib/cache.d.ts","directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^1.11.1","@actions/exec":"^1.0.1","@actions/glob":"^0.1.0","@actions/http-client":"^2.1.1","@actions/io":"^1.0.1","@azure/abort-controller":"^1.1.0","@azure/ms-rest-js":"^2.6.0","@azure/storage-blob":"^12.13.0","@protobuf-ts/plugin":"^2.9.4","semver":"^6.3.1"},"devDependencies":{"@types/semver":"^6.0.0","typescript":"^5.2.2"}}');
+module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"4.0.3","preview":true,"description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","main":"lib/cache.js","types":"lib/cache.d.ts","directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^1.11.1","@actions/exec":"^1.0.1","@actions/glob":"^0.1.0","@actions/http-client":"^2.1.1","@actions/io":"^1.0.1","@azure/abort-controller":"^1.1.0","@azure/ms-rest-js":"^2.6.0","@azure/storage-blob":"^12.13.0","@protobuf-ts/plugin":"^2.9.4","semver":"^6.3.1"},"devDependencies":{"@types/node":"^22.13.9","@types/semver":"^6.0.0","typescript":"^5.2.2"}}');
 
 /***/ }),
 
diff --git dist/setup/index.js dist/setup/index.js
index 93bc3bb64..81bbe3faf 100644
--- dist/setup/index.js
+++ dist/setup/index.js
@@ -220,7 +220,7 @@ function restoreCacheV2(paths, primaryKey, restoreKeys, options, enableCrossOsAr
             };
             const response = yield twirpClient.GetCacheEntryDownloadURL(request);
             if (!response.ok) {
-                core.debug(`Cache not found for keys: ${keys.join(', ')}`);
+                core.debug(`Cache not found for version ${request.version} of keys: ${keys.join(', ')}`);
                 return undefined;
             }
             core.info(`Cache hit for: ${request.key}`);
@@ -2204,6 +2204,7 @@ const cacheUtils_1 = __nccwpck_require__(680);
 const auth_1 = __nccwpck_require__(4552);
 const http_client_1 = __nccwpck_require__(4844);
 const cache_twirp_client_1 = __nccwpck_require__(1486);
+const util_1 = __nccwpck_require__(7564);
 /**
  * This class is a wrapper around the CacheServiceClientJSON class generated by Twirp.
  *
@@ -2263,6 +2264,7 @@ class CacheServiceClient {
                     (0, core_1.debug)(`[Response] - ${response.message.statusCode}`);
                     (0, core_1.debug)(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`);
                     const body = JSON.parse(rawBody);
+                    (0, util_1.maskSecretUrls)(body);
                     (0, core_1.debug)(`Body: ${JSON.stringify(body, null, 2)}`);
                     if (this.isSuccessStatusCode(statusCode)) {
                         return { response, body };
@@ -2444,6 +2446,87 @@ exports.getUserAgentString = getUserAgentString;
 
 /***/ }),
 
+/***/ 7564:
+/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.maskSecretUrls = exports.maskSigUrl = void 0;
+const core_1 = __nccwpck_require__(7484);
+/**
+ * Masks the `sig` parameter in a URL and sets it as a secret.
+ *
+ * @param url - The URL containing the signature parameter to mask
+ * @remarks
+ * This function attempts to parse the provided URL and identify the 'sig' query parameter.
+ * If found, it registers both the raw and URL-encoded signature values as secrets using
+ * the Actions `setSecret` API, which prevents them from being displayed in logs.
+ *
+ * The function handles errors gracefully if URL parsing fails, logging them as debug messages.
+ *
+ * @example
+ * ```typescript
+ * // Mask a signature in an Azure SAS token URL
+ * maskSigUrl('https://example.blob.core.windows.net/container/file.txt?sig=abc123&se=2023-01-01');
+ * ```
+ */
+function maskSigUrl(url) {
+    if (!url)
+        return;
+    try {
+        const parsedUrl = new URL(url);
+        const signature = parsedUrl.searchParams.get('sig');
+        if (signature) {
+            (0, core_1.setSecret)(signature);
+            (0, core_1.setSecret)(encodeURIComponent(signature));
+        }
+    }
+    catch (error) {
+        (0, core_1.debug)(`Failed to parse URL: ${url} ${error instanceof Error ? error.message : String(error)}`);
+    }
+}
+exports.maskSigUrl = maskSigUrl;
+/**
+ * Masks sensitive information in URLs containing signature parameters.
+ * Currently supports masking 'sig' parameters in the 'signed_upload_url'
+ * and 'signed_download_url' properties of the provided object.
+ *
+ * @param body - The object should contain a signature
+ * @remarks
+ * This function extracts URLs from the object properties and calls maskSigUrl
+ * on each one to redact sensitive signature information. The function doesn't
+ * modify the original object; it only marks the signatures as secrets for
+ * logging purposes.
+ *
+ * @example
+ * ```typescript
+ * const responseBody = {
+ *   signed_upload_url: 'https://blob.core.windows.net/?sig=abc123',
+ *   signed_download_url: 'https://blob.core/windows.net/?sig=def456'
+ * };
+ * maskSecretUrls(responseBody);
+ * ```
+ */
+function maskSecretUrls(body) {
+    if (typeof body !== 'object' || body === null) {
+        (0, core_1.debug)('body is not an object or is null');
+        return;
+    }
+    if ('signed_upload_url' in body &&
+        typeof body.signed_upload_url === 'string') {
+        maskSigUrl(body.signed_upload_url);
+    }
+    if ('signed_download_url' in body &&
+        typeof body.signed_download_url === 'string') {
+        maskSigUrl(body.signed_download_url);
+    }
+}
+exports.maskSecretUrls = maskSecretUrls;
+//# sourceMappingURL=util.js.map
+
+/***/ }),
+
 /***/ 5321:
 /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
 
@@ -88838,7 +88921,7 @@ function getUserAgent() {
     return navigator.userAgent;
   }
 
-  if (typeof process === "object" && "version" in process) {
+  if (typeof process === "object" && process.version !== undefined) {
     return `Node.js/${process.version.substr(1)} (${process.platform}; ${process.arch})`;
   }
 
@@ -97211,9 +97294,13 @@ class BaseDistribution {
     }
     getNodeJsVersions() {
         return __awaiter(this, void 0, void 0, function* () {
-            const initialUrl = this.getDistributionUrl();
+            const initialUrl = this.getDistributionUrl(this.nodeInfo.mirror);
             const dataUrl = `${initialUrl}/index.json`;
-            const response = yield this.httpClient.getJson(dataUrl);
+            const headers = {};
+            if (this.nodeInfo.mirrorToken) {
+                headers['Authorization'] = `Bearer ${this.nodeInfo.mirrorToken}`;
+            }
+            const response = yield this.httpClient.getJson(dataUrl, headers);
             return response.result || [];
         });
     }
@@ -97228,7 +97315,7 @@ class BaseDistribution {
                 ? `${fileName}.zip`
                 : `${fileName}.7z`
             : `${fileName}.tar.gz`;
-        const initialUrl = this.getDistributionUrl();
+        const initialUrl = this.getDistributionUrl(this.nodeInfo.mirror);
         const url = `${initialUrl}/v${version}/${urlFileName}`;
         return {
             downloadUrl: url,
@@ -97242,7 +97329,7 @@ class BaseDistribution {
             let downloadPath = '';
             core.info(`Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}`);
             try {
-                downloadPath = yield tc.downloadTool(info.downloadUrl);
+                downloadPath = yield tc.downloadTool(info.downloadUrl, undefined, this.nodeInfo.mirrorToken);
             }
             catch (err) {
                 if (err instanceof tc.HTTPError &&
@@ -97266,7 +97353,7 @@ class BaseDistribution {
     }
     acquireWindowsNodeFromFallbackLocation(version_1) {
         return __awaiter(this, arguments, void 0, function* (version, arch = os_1.default.arch()) {
-            const initialUrl = this.getDistributionUrl();
+            const initialUrl = this.getDistributionUrl(this.nodeInfo.mirror);
             const osArch = this.translateArchToDistUrl(arch);
             // Create temporary folder to download to
             const tempDownloadFolder = `temp_${(0, uuid_1.v4)()}`;
@@ -97280,18 +97367,18 @@ class BaseDistribution {
                 exeUrl = `${initialUrl}/v${version}/win-${osArch}/node.exe`;
                 libUrl = `${initialUrl}/v${version}/win-${osArch}/node.lib`;
                 core.info(`Downloading only node binary from ${exeUrl}`);
-                const exePath = yield tc.downloadTool(exeUrl);
+                const exePath = yield tc.downloadTool(exeUrl, undefined, this.nodeInfo.mirrorToken);
                 yield io.cp(exePath, path.join(tempDir, 'node.exe'));
-                const libPath = yield tc.downloadTool(libUrl);
+                const libPath = yield tc.downloadTool(libUrl, undefined, this.nodeInfo.mirrorToken);
                 yield io.cp(libPath, path.join(tempDir, 'node.lib'));
             }
             catch (err) {
                 if (err instanceof tc.HTTPError && err.httpStatusCode == 404) {
                     exeUrl = `${initialUrl}/v${version}/node.exe`;
                     libUrl = `${initialUrl}/v${version}/node.lib`;
-                    const exePath = yield tc.downloadTool(exeUrl);
+                    const exePath = yield tc.downloadTool(exeUrl, undefined, this.nodeInfo.mirrorToken);
                     yield io.cp(exePath, path.join(tempDir, 'node.exe'));
-                    const libPath = yield tc.downloadTool(libUrl);
+                    const libPath = yield tc.downloadTool(libUrl, undefined, this.nodeInfo.mirrorToken);
                     yield io.cp(libPath, path.join(tempDir, 'node.lib'));
                 }
                 else {
@@ -97454,8 +97541,9 @@ class NightlyNodejs extends base_distribution_prerelease_1.default {
         super(nodeInfo);
         this.distribution = 'nightly';
     }
-    getDistributionUrl() {
-        return 'https://nodejs.org/download/nightly';
+    getDistributionUrl(mirror) {
+        const url = mirror || 'https://nodejs.org';
+        return `${url}/download/nightly`;
     }
 }
 exports["default"] = NightlyNodejs;
@@ -97553,13 +97641,13 @@ class OfficialBuilds extends base_distribution_1.default {
                 const versionInfo = yield this.getInfoFromManifest(this.nodeInfo.versionSpec, this.nodeInfo.stable, osArch, manifest);
                 if (versionInfo) {
                     core.info(`Acquiring ${versionInfo.resolvedVersion} - ${versionInfo.arch} from ${versionInfo.downloadUrl}`);
-                    downloadPath = yield tc.downloadTool(versionInfo.downloadUrl, undefined, this.nodeInfo.auth);
+                    downloadPath = yield tc.downloadTool(versionInfo.downloadUrl, undefined, this.nodeInfo.mirror ? this.nodeInfo.mirrorToken : this.nodeInfo.auth);
                     if (downloadPath) {
                         toolPath = yield this.extractArchive(downloadPath, versionInfo, false);
                     }
                 }
                 else {
-                    core.info('Not found in manifest. Falling back to download directly from Node');
+                    core.info(`Not found in manifest. Falling back to download directly from ${this.nodeInfo.mirror || 'Node'}`);
                 }
             }
             catch (err) {
@@ -97621,12 +97709,13 @@ class OfficialBuilds extends base_distribution_1.default {
         version = super.evaluateVersions(versions);
         return version;
     }
-    getDistributionUrl() {
-        return `https://nodejs.org/dist`;
+    getDistributionUrl(mirror) {
+        const url = mirror || 'https://nodejs.org';
+        return `${url}/dist`;
     }
     getManifest() {
         core.debug('Getting manifest from actions/node-versions@main');
-        return tc.getManifestFromRepo('actions', 'node-versions', this.nodeInfo.auth, 'main');
+        return tc.getManifestFromRepo('actions', 'node-versions', this.nodeInfo.mirror ? this.nodeInfo.mirrorToken : this.nodeInfo.auth, 'main');
     }
     resolveLtsAliasFromManifest(versionSpec, stable, manifest) {
         var _a;
@@ -97709,8 +97798,9 @@ class RcBuild extends base_distribution_1.default {
     constructor(nodeInfo) {
         super(nodeInfo);
     }
-    getDistributionUrl() {
-        return 'https://nodejs.org/download/rc';
+    getDistributionUrl(mirror) {
+        const url = mirror || 'https://nodejs.org';
+        return `${url}/download/rc`;
     }
 }
 exports["default"] = RcBuild;
@@ -97733,8 +97823,9 @@ class CanaryBuild extends base_distribution_prerelease_1.default {
         super(nodeInfo);
         this.distribution = 'v8-canary';
     }
-    getDistributionUrl() {
-        return 'https://nodejs.org/download/v8-canary';
+    getDistributionUrl(mirror) {
+        const url = mirror || 'https://nodejs.org';
+        return `${url}/download/v8-canary`;
     }
 }
 exports["default"] = CanaryBuild;
@@ -97814,6 +97905,8 @@ function run() {
             if (version) {
                 const token = core.getInput('token');
                 const auth = !token ? undefined : `token ${token}`;
+                const mirror = core.getInput('mirror');
+                const mirrorToken = core.getInput('mirror-token');
                 const stable = (core.getInput('stable') || 'true').toUpperCase() === 'TRUE';
                 const checkLatest = (core.getInput('check-latest') || 'false').toUpperCase() === 'TRUE';
                 const nodejsInfo = {
@@ -97821,7 +97914,9 @@ function run() {
                     checkLatest,
                     auth,
                     stable,
-                    arch
+                    arch,
+                    mirror,
+                    mirrorToken
                 };
                 const nodeDistribution = (0, installer_factory_1.getNodejsDistribution)(nodejsInfo);
                 yield nodeDistribution.setupNodeJs();
@@ -99910,7 +100005,7 @@ module.exports = parseParams
 /***/ ((module) => {
 
 "use strict";
-module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"4.0.2","preview":true,"description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","main":"lib/cache.js","types":"lib/cache.d.ts","directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^1.11.1","@actions/exec":"^1.0.1","@actions/glob":"^0.1.0","@actions/http-client":"^2.1.1","@actions/io":"^1.0.1","@azure/abort-controller":"^1.1.0","@azure/ms-rest-js":"^2.6.0","@azure/storage-blob":"^12.13.0","@protobuf-ts/plugin":"^2.9.4","semver":"^6.3.1"},"devDependencies":{"@types/semver":"^6.0.0","typescript":"^5.2.2"}}');
+module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"4.0.3","preview":true,"description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","main":"lib/cache.js","types":"lib/cache.d.ts","directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^1.11.1","@actions/exec":"^1.0.1","@actions/glob":"^0.1.0","@actions/http-client":"^2.1.1","@actions/io":"^1.0.1","@azure/abort-controller":"^1.1.0","@azure/ms-rest-js":"^2.6.0","@azure/storage-blob":"^12.13.0","@protobuf-ts/plugin":"^2.9.4","semver":"^6.3.1"},"devDependencies":{"@types/node":"^22.13.9","@types/semver":"^6.0.0","typescript":"^5.2.2"}}');
 
 /***/ }),
 
diff --git docs/advanced-usage.md docs/advanced-usage.md
index bf62e0713..856c5efac 100644
--- docs/advanced-usage.md
+++ docs/advanced-usage.md
@@ -418,3 +418,18 @@ Please refer to the [Ensuring workflow access to your package - Configuring a pa
 
 ### always-auth input
 The always-auth input sets `always-auth=true` in .npmrc file. With this option set [npm](https://docs.npmjs.com/cli/v6/using-npm/config#always-auth)/yarn sends the authentication credentials when making a request to the registries.
+
+## Use private mirror
+
+It is possible to use a private mirror hosting Node.js binaries. This mirror must be a full mirror of the official Node.js distribution.
+The mirror URL can be set using the `mirror` input.
+It is possible to specify a token to authenticate with the mirror using the `mirror-token` input.
+The token will be passed as a bearer token in the `Authorization` header.
+
+```yaml
+- uses: actions/setup-node@v4
+  with:
+    node-version: '14.x'
+    mirror: 'https://nodejs.org/dist'
+    mirror-token: 'your-mirror-token'
+```
diff --git package.json package.json
index 5fb0c3873..0abcfe381 100644
--- package.json
+++ package.json
@@ -25,7 +25,7 @@
   "author": "GitHub",
   "license": "MIT",
   "dependencies": {
-    "@actions/cache": "^4.0.2",
+    "@actions/cache": "^4.0.3",
     "@actions/core": "^1.11.1",
     "@actions/exec": "^1.1.1",
     "@actions/github": "^5.1.1",
diff --git src/distributions/base-distribution.ts src/distributions/base-distribution.ts
index 70b4b5724..0a99f3a8c 100644
--- src/distributions/base-distribution.ts
+++ src/distributions/base-distribution.ts
@@ -24,7 +24,7 @@ export default abstract class BaseDistribution {
     });
   }
 
-  protected abstract getDistributionUrl(): string;
+  protected abstract getDistributionUrl(mirror: string): string;
 
   public async setupNodeJs() {
     let nodeJsVersions: INodeVersion[] | undefined;
@@ -97,10 +97,19 @@ export default abstract class BaseDistribution {
   }
 
   protected async getNodeJsVersions(): Promise<INodeVersion[]> {
-    const initialUrl = this.getDistributionUrl();
+    const initialUrl = this.getDistributionUrl(this.nodeInfo.mirror);
     const dataUrl = `${initialUrl}/index.json`;
 
-    const response = await this.httpClient.getJson<INodeVersion[]>(dataUrl);
+    const headers = {};
+
+    if (this.nodeInfo.mirrorToken) {
+      headers['Authorization'] = `Bearer ${this.nodeInfo.mirrorToken}`;
+    }
+
+    const response = await this.httpClient.getJson<INodeVersion[]>(
+      dataUrl,
+      headers
+    );
     return response.result || [];
   }
 
@@ -117,7 +126,7 @@ export default abstract class BaseDistribution {
           ? `${fileName}.zip`
           : `${fileName}.7z`
         : `${fileName}.tar.gz`;
-    const initialUrl = this.getDistributionUrl();
+    const initialUrl = this.getDistributionUrl(this.nodeInfo.mirror);
     const url = `${initialUrl}/v${version}/${urlFileName}`;
 
     return <INodeVersionInfo>{
@@ -134,7 +143,11 @@ export default abstract class BaseDistribution {
       `Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}`
     );
     try {
-      downloadPath = await tc.downloadTool(info.downloadUrl);
+      downloadPath = await tc.downloadTool(
+        info.downloadUrl,
+        undefined,
+        this.nodeInfo.mirrorToken
+      );
     } catch (err) {
       if (
         err instanceof tc.HTTPError &&
@@ -168,7 +181,7 @@ export default abstract class BaseDistribution {
     version: string,
     arch: string = os.arch()
   ): Promise<string> {
-    const initialUrl = this.getDistributionUrl();
+    const initialUrl = this.getDistributionUrl(this.nodeInfo.mirror);
     const osArch: string = this.translateArchToDistUrl(arch);
 
     // Create temporary folder to download to
@@ -185,18 +198,34 @@ export default abstract class BaseDistribution {
 
       core.info(`Downloading only node binary from ${exeUrl}`);
 
-      const exePath = await tc.downloadTool(exeUrl);
+      const exePath = await tc.downloadTool(
+        exeUrl,
+        undefined,
+        this.nodeInfo.mirrorToken
+      );
       await io.cp(exePath, path.join(tempDir, 'node.exe'));
-      const libPath = await tc.downloadTool(libUrl);
+      const libPath = await tc.downloadTool(
+        libUrl,
+        undefined,
+        this.nodeInfo.mirrorToken
+      );
       await io.cp(libPath, path.join(tempDir, 'node.lib'));
     } catch (err) {
       if (err instanceof tc.HTTPError && err.httpStatusCode == 404) {
         exeUrl = `${initialUrl}/v${version}/node.exe`;
         libUrl = `${initialUrl}/v${version}/node.lib`;
 
-        const exePath = await tc.downloadTool(exeUrl);
+        const exePath = await tc.downloadTool(
+          exeUrl,
+          undefined,
+          this.nodeInfo.mirrorToken
+        );
         await io.cp(exePath, path.join(tempDir, 'node.exe'));
-        const libPath = await tc.downloadTool(libUrl);
+        const libPath = await tc.downloadTool(
+          libUrl,
+          undefined,
+          this.nodeInfo.mirrorToken
+        );
         await io.cp(libPath, path.join(tempDir, 'node.lib'));
       } else {
         throw err;
diff --git src/distributions/base-models.ts src/distributions/base-models.ts
index 0be93b635..61778cf9b 100644
--- src/distributions/base-models.ts
+++ src/distributions/base-models.ts
@@ -4,6 +4,8 @@ export interface NodeInputs {
   auth?: string;
   checkLatest: boolean;
   stable: boolean;
+  mirror: string;
+  mirrorToken: string;
 }
 
 export interface INodeVersionInfo {
diff --git src/distributions/nightly/nightly_builds.ts src/distributions/nightly/nightly_builds.ts
index 86a89eed9..b3c366fa7 100644
--- src/distributions/nightly/nightly_builds.ts
+++ src/distributions/nightly/nightly_builds.ts
@@ -7,7 +7,8 @@ export default class NightlyNodejs extends BasePrereleaseNodejs {
     super(nodeInfo);
   }
 
-  protected getDistributionUrl(): string {
-    return 'https://nodejs.org/download/nightly';
+  protected getDistributionUrl(mirror: string): string {
+    const url = mirror || 'https://nodejs.org';
+    return `${url}/download/nightly`;
   }
 }
diff --git src/distributions/official_builds/official_builds.ts src/distributions/official_builds/official_builds.ts
index e56eaf812..62999c334 100644
--- src/distributions/official_builds/official_builds.ts
+++ src/distributions/official_builds/official_builds.ts
@@ -84,7 +84,7 @@ export default class OfficialBuilds extends BaseDistribution {
         downloadPath = await tc.downloadTool(
           versionInfo.downloadUrl,
           undefined,
-          this.nodeInfo.auth
+          this.nodeInfo.mirror ? this.nodeInfo.mirrorToken : this.nodeInfo.auth
         );
 
         if (downloadPath) {
@@ -96,7 +96,9 @@ export default class OfficialBuilds extends BaseDistribution {
         }
       } else {
         core.info(
-          'Not found in manifest. Falling back to download directly from Node'
+          `Not found in manifest. Falling back to download directly from ${
+            this.nodeInfo.mirror || 'Node'
+          }`
         );
       }
     } catch (err) {
@@ -176,8 +178,9 @@ export default class OfficialBuilds extends BaseDistribution {
     return version;
   }
 
-  protected getDistributionUrl(): string {
-    return `https://nodejs.org/dist`;
+  protected getDistributionUrl(mirror: string): string {
+    const url = mirror || 'https://nodejs.org';
+    return `${url}/dist`;
   }
 
   private getManifest(): Promise<tc.IToolRelease[]> {
@@ -185,7 +188,7 @@ export default class OfficialBuilds extends BaseDistribution {
     return tc.getManifestFromRepo(
       'actions',
       'node-versions',
-      this.nodeInfo.auth,
+      this.nodeInfo.mirror ? this.nodeInfo.mirrorToken : this.nodeInfo.auth,
       'main'
     );
   }
diff --git src/distributions/rc/rc_builds.ts src/distributions/rc/rc_builds.ts
index 40cdb192a..38a6b0166 100644
--- src/distributions/rc/rc_builds.ts
+++ src/distributions/rc/rc_builds.ts
@@ -6,7 +6,8 @@ export default class RcBuild extends BaseDistribution {
     super(nodeInfo);
   }
 
-  getDistributionUrl(): string {
-    return 'https://nodejs.org/download/rc';
+  getDistributionUrl(mirror: string): string {
+    const url = mirror || 'https://nodejs.org';
+    return `${url}/download/rc`;
   }
 }
diff --git src/distributions/v8-canary/canary_builds.ts src/distributions/v8-canary/canary_builds.ts
index 257151b45..b714b67d4 100644
--- src/distributions/v8-canary/canary_builds.ts
+++ src/distributions/v8-canary/canary_builds.ts
@@ -7,7 +7,8 @@ export default class CanaryBuild extends BasePrereleaseNodejs {
     super(nodeInfo);
   }
 
-  protected getDistributionUrl(): string {
-    return 'https://nodejs.org/download/v8-canary';
+  protected getDistributionUrl(mirror: string): string {
+    const url = mirror || 'https://nodejs.org';
+    return `${url}/download/v8-canary`;
   }
 }
diff --git src/main.ts src/main.ts
index c55c3b005..c36d8ec5a 100644
--- src/main.ts
+++ src/main.ts
@@ -36,6 +36,8 @@ export async function run() {
     if (version) {
       const token = core.getInput('token');
       const auth = !token ? undefined : `token ${token}`;
+      const mirror = core.getInput('mirror');
+      const mirrorToken = core.getInput('mirror-token');
       const stable =
         (core.getInput('stable') || 'true').toUpperCase() === 'TRUE';
       const checkLatest =
@@ -45,7 +47,9 @@ export async function run() {
         checkLatest,
         auth,
         stable,
-        arch
+        arch,
+        mirror,
+        mirrorToken
       };
       const nodeDistribution = getNodejsDistribution(nodejsInfo);
       await nodeDistribution.setupNodeJs();

Description

This PR adds support for downloading Node.js binaries from alternative mirrors by introducing two new input parameters: mirror and mirror-token. The implementation allows users to specify a custom mirror URL and an optional authentication token for accessing private mirrors. This feature is useful for users in regions where access to the default Node.js download servers might be restricted or for organizations that maintain their own Node.js binary mirrors.

Changes

Changes

  1. README.md and docs/advanced-usage.md

    • Added documentation for the new mirror and mirror-token inputs
    • Included examples of how to use the mirror feature
  2. action.yml

    • Added new input parameters mirror and mirror-token to the action's configuration
  3. Distribution classes

    • Modified getDistributionUrl() method in all distribution classes to accept a mirror parameter
    • Updated URL construction to use the provided mirror when available
    • Added support for authentication with private mirrors
  4. NodeInputs interface

    • Added mirror and mirrorToken fields to the NodeInputs interface
  5. Download functionality

    • Updated the download tool calls to pass the mirror token when specified
    • Modified HTTP request logic to include authorization headers when using mirrors
  6. Regular expression updates

    • Enhanced regular expressions in .github/eslint-compact.json and .github/eslint-stylish.json for better case-insensitive matching
  7. Dependency updates

    • Updated @actions/cache from 4.0.2 to 4.0.3
    • Updated universal-user-agent from 6.0.0 to 6.0.1
  8. Tests

    • Added new tests to verify mirror functionality for various Node.js distribution types
sequenceDiagram
    actor User
    participant Action as setup-node Action
    participant Distribution as Node Distribution Classes
    participant Mirror as Custom Mirror
    participant DefaultMirror as Default Node.js Servers
    participant Tool as tool-cache

    User->>Action: Run with mirror parameters
    Action->>Action: Parse inputs (mirror, mirror-token)
    Action->>Distribution: Create distribution with mirror config
    
    alt mirror is specified
        Distribution->>Mirror: Request Node.js version info with mirror-token
        Mirror->>Distribution: Return version data
        
        Distribution->>Mirror: Download Node.js binary with mirror-token
        Mirror->>Distribution: Return binary data
    else mirror not specified
        Distribution->>DefaultMirror: Request Node.js version info
        DefaultMirror->>Distribution: Return version data
        
        Distribution->>DefaultMirror: Download Node.js binary
        DefaultMirror->>Distribution: Return binary data
    end
    
    Distribution->>Tool: Extract and cache binary
    Tool->>Distribution: Return tool path
    Distribution->>Action: Return success
    Action->>User: Node.js successfully set up
Loading

Copy link
Contributor

👋 Thanks for Submitting! This PR is available for preview at the link below.

✅ PR tip preview: https://1058.pr.nala.bravesoftware.com/
✅ Commit preview: https://1058.pr.nala.bravesoftware.com/commit-ab1245b06e930c054d4977b336696202db6882c6/

- ./tokens/css/variables-android.old.css: 7390 bytes
+ ./tokens/css/variables-android.css: 7390 bytes
---
- ./tokens/css/variables-browser.old.css: 6644 bytes
+ ./tokens/css/variables-browser.css: 6644 bytes
---
- ./tokens/css/variables-ios.old.css: 7033 bytes
+ ./tokens/css/variables-ios.css: 7033 bytes
---
- ./tokens/css/variables-marketing.old.css: 13501 bytes
+ ./tokens/css/variables-marketing.css: 13501 bytes
---
- ./tokens/css/variables-news.old.css: 526 bytes
+ ./tokens/css/variables-news.css: 526 bytes
---
- ./tokens/css/variables-newtab.old.css: 1933 bytes
+ ./tokens/css/variables-newtab.css: 1933 bytes
---
- ./tokens/css/variables-search.old.css: 2409 bytes
+ ./tokens/css/variables-search.css: 2409 bytes
---
- ./tokens/css/variables-web3.old.css: 893 bytes
+ ./tokens/css/variables-web3.css: 893 bytes
---
- ./tokens/css/variables.old.css: 120612 bytes
+ ./tokens/css/variables.css: 120612 bytes
Variables Diff: variables-android.diff
--- ./tokens/css/variables-android.old.css	2025-04-15 02:48:46.987925962 +0000
+++ ./tokens/css/variables-android.css	2025-04-15 02:48:15.215041041 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 :root {
Variables Diff: variables-browser.diff
--- ./tokens/css/variables-browser.old.css	2025-04-15 02:48:47.196925227 +0000
+++ ./tokens/css/variables-browser.css	2025-04-15 02:48:15.203041075 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 :root {
Variables Diff: variables-ios.diff
--- ./tokens/css/variables-ios.old.css	2025-04-15 02:48:47.521924081 +0000
+++ ./tokens/css/variables-ios.css	2025-04-15 02:48:15.237040979 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 :root {
Variables Diff: variables-marketing.diff
--- ./tokens/css/variables-marketing.old.css	2025-04-15 02:48:47.752923269 +0000
+++ ./tokens/css/variables-marketing.css	2025-04-15 02:48:15.253040934 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 :root {
Variables Diff: variables-news.diff
--- ./tokens/css/variables-news.old.css	2025-04-15 02:48:47.985922446 +0000
+++ ./tokens/css/variables-news.css	2025-04-15 02:48:15.272040880 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 :root {
Variables Diff: variables-newtab.diff
--- ./tokens/css/variables-newtab.old.css	2025-04-15 02:48:48.191921700 +0000
+++ ./tokens/css/variables-newtab.css	2025-04-15 02:48:15.279040860 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 :root {
Variables Diff: variables-search.diff
--- ./tokens/css/variables-search.old.css	2025-04-15 02:48:48.376921031 +0000
+++ ./tokens/css/variables-search.css	2025-04-15 02:48:15.268040892 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 :root {
Variables Diff: variables-web3.diff
--- ./tokens/css/variables-web3.old.css	2025-04-15 02:48:48.578920301 +0000
+++ ./tokens/css/variables-web3.css	2025-04-15 02:48:15.283040849 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 @media (prefers-color-scheme: light) {
Variables Diff: variables.diff
--- ./tokens/css/variables.old.css	2025-04-15 02:48:48.889919178 +0000
+++ ./tokens/css/variables.css	2025-04-15 02:48:15.079041425 +0000
@@ -1,6 +1,6 @@
 /**
  * Do not edit directly
- * Generated on Wed Apr 09 2025 19:26:34 GMT+0000 (Coordinated Universal Time)
+ * Generated on Tue Apr 15 2025 02:48:15 GMT+0000 (Coordinated Universal Time)
  */
 
 :root {

@renovate renovate bot changed the title chore(deps): update actions/setup-node action to v4.4.0 chore(deps): update actions/setup-node action to v4.4.0 - autoclosed Apr 15, 2025
@renovate renovate bot closed this Apr 15, 2025
auto-merge was automatically disabled April 15, 2025 05:37

Pull request was closed

@renovate renovate bot deleted the renovate/actions-setup-node-4.x branch April 15, 2025 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dependencies Pull requests that update a dependency file puLL-Merge renovate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0 participants