From 8f2ef77ada4fa1622c60794031729a6db424af73 Mon Sep 17 00:00:00 2001
From: Marco Ippolito <marcoippolito54@gmail.com>
Date: Thu, 6 Mar 2025 10:52:23 +0100
Subject: [PATCH 1/2] feat: support private mirrors

---
 README.md                                     | 15 +++++
 __tests__/canary-installer.test.ts            | 64 +++++++++++++++++++
 __tests__/nightly-installer.test.ts           | 50 ++++++++++++++-
 __tests__/official-installer.test.ts          | 42 ++++++++++++
 action.yml                                    |  4 ++
 dist/setup/index.js                           | 52 +++++++++------
 docs/advanced-usage.md                        | 15 +++++
 src/distributions/base-distribution.ts        | 49 +++++++++++---
 src/distributions/base-models.ts              |  2 +
 src/distributions/nightly/nightly_builds.ts   |  5 +-
 .../official_builds/official_builds.ts        |  9 +--
 src/distributions/rc/rc_builds.ts             |  5 +-
 src/distributions/v8-canary/canary_builds.ts  |  5 +-
 src/main.ts                                   |  6 +-
 14 files changed, 281 insertions(+), 42 deletions(-)

diff --git a/README.md b/README.md
index 0c554898..92804e94 100644
--- a/README.md
+++ b/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 a/__tests__/canary-installer.test.ts b/__tests__/canary-installer.test.ts
index 6d141fc3..4393d7ef 100644
--- a/__tests__/canary-installer.test.ts
+++ b/__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 a/__tests__/nightly-installer.test.ts b/__tests__/nightly-installer.test.ts
index 87c43795..eece2c34 100644
--- a/__tests__/nightly-installer.test.ts
+++ b/__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 a/__tests__/official-installer.test.ts b/__tests__/official-installer.test.ts
index 2d8f17cf..6b471c5d 100644
--- a/__tests__/official-installer.test.ts
+++ b/__tests__/official-installer.test.ts
@@ -828,4 +828,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 a/action.yml b/action.yml
index 99db5869..ef58e699 100644
--- a/action.yml
+++ b/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 a/dist/setup/index.js b/dist/setup/index.js
index 93bc3bb6..0207df45 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -97211,9 +97211,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 +97232,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 +97246,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 +97270,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 +97284,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 +97458,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,7 +97558,7 @@ 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);
                     }
@@ -97621,12 +97626,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 +97715,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 +97740,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 +97822,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 +97831,9 @@ function run() {
                     checkLatest,
                     auth,
                     stable,
-                    arch
+                    arch,
+                    mirror,
+                    mirrorToken
                 };
                 const nodeDistribution = (0, installer_factory_1.getNodejsDistribution)(nodejsInfo);
                 yield nodeDistribution.setupNodeJs();
diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md
index bf62e071..856c5efa 100644
--- a/docs/advanced-usage.md
+++ b/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 a/src/distributions/base-distribution.ts b/src/distributions/base-distribution.ts
index 70b4b572..0a99f3a8 100644
--- a/src/distributions/base-distribution.ts
+++ b/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 a/src/distributions/base-models.ts b/src/distributions/base-models.ts
index 0be93b63..61778cf9 100644
--- a/src/distributions/base-models.ts
+++ b/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 a/src/distributions/nightly/nightly_builds.ts b/src/distributions/nightly/nightly_builds.ts
index 86a89eed..b3c366fa 100644
--- a/src/distributions/nightly/nightly_builds.ts
+++ b/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 a/src/distributions/official_builds/official_builds.ts b/src/distributions/official_builds/official_builds.ts
index e56eaf81..d78a4966 100644
--- a/src/distributions/official_builds/official_builds.ts
+++ b/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) {
@@ -176,8 +176,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 +186,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 a/src/distributions/rc/rc_builds.ts b/src/distributions/rc/rc_builds.ts
index 40cdb192..38a6b016 100644
--- a/src/distributions/rc/rc_builds.ts
+++ b/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 a/src/distributions/v8-canary/canary_builds.ts b/src/distributions/v8-canary/canary_builds.ts
index 257151b4..b714b67d 100644
--- a/src/distributions/v8-canary/canary_builds.ts
+++ b/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 a/src/main.ts b/src/main.ts
index c55c3b00..c36d8ec5 100644
--- a/src/main.ts
+++ b/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();

From 40509ec00131554bce93071a3c3ed77e5b93a4d5 Mon Sep 17 00:00:00 2001
From: Marco Ippolito <marcoippolito54@gmail.com>
Date: Thu, 27 Mar 2025 14:29:32 +0100
Subject: [PATCH 2/2] chore: change fallback message with mirrors

---
 __tests__/official-installer.test.ts          | 37 +++++++++++++++++++
 dist/setup/index.js                           |  8 ++--
 .../official_builds/official_builds.ts        | 10 ++++-
 3 files changed, 49 insertions(+), 6 deletions(-)

diff --git a/__tests__/official-installer.test.ts b/__tests__/official-installer.test.ts
index 6b471c5d..f23183d3 100644
--- a/__tests__/official-installer.test.ts
+++ b/__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';
diff --git a/dist/setup/index.js b/dist/setup/index.js
index 0207df45..c43d61fe 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -97519,7 +97519,7 @@ class OfficialBuilds extends base_distribution_1.default {
     }
     setupNodeJs() {
         return __awaiter(this, void 0, void 0, function* () {
-            var _a;
+            var _a, _b, _c;
             let manifest;
             let nodeJsVersions;
             const osArch = this.translateArchToDistUrl(this.nodeInfo.arch);
@@ -97564,7 +97564,7 @@ class OfficialBuilds extends base_distribution_1.default {
                     }
                 }
                 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 ${(_a = this.nodeInfo.mirror) !== null && _a !== void 0 ? _a : 'Node'}`);
                 }
             }
             catch (err) {
@@ -97576,8 +97576,8 @@ class OfficialBuilds extends base_distribution_1.default {
                 else {
                     core.info(err.message);
                 }
-                core.debug((_a = err.stack) !== null && _a !== void 0 ? _a : 'empty stack');
-                core.info('Falling back to download directly from Node');
+                core.debug((_b = err.stack) !== null && _b !== void 0 ? _b : 'empty stack');
+                core.info(`Falling back to download directly from ${(_c = this.nodeInfo.mirror) !== null && _c !== void 0 ? _c : 'Node'}`);
             }
             if (!toolPath) {
                 toolPath = yield this.downloadDirectlyFromNode();
diff --git a/src/distributions/official_builds/official_builds.ts b/src/distributions/official_builds/official_builds.ts
index d78a4966..abd1ede1 100644
--- a/src/distributions/official_builds/official_builds.ts
+++ b/src/distributions/official_builds/official_builds.ts
@@ -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) {
@@ -112,7 +114,11 @@ export default class OfficialBuilds extends BaseDistribution {
         core.info((err as Error).message);
       }
       core.debug((err as Error).stack ?? 'empty stack');
-      core.info('Falling back to download directly from Node');
+      core.info(
+        `Falling back to download directly from ${
+          this.nodeInfo.mirror ?? 'Node'
+        }`
+      );
     }
 
     if (!toolPath) {