diff --git a/__tests__/canary-installer.test.ts b/__tests__/canary-installer.test.ts index 6d141fc3..ef38c714 100644 --- a/__tests__/canary-installer.test.ts +++ b/__tests__/canary-installer.test.ts @@ -10,13 +10,14 @@ import osm from 'os'; import path from 'path'; import * as main from '../src/main'; import * as auth from '../src/authutil'; -import {INodeVersion} from '../src/distributions/base-models'; +import {INodeVersion, NodeInputs} from '../src/distributions/base-models'; import nodeTestManifest from './data/versions-manifest.json'; import nodeTestDist from './data/node-dist-index.json'; import nodeTestDistNightly from './data/node-nightly-index.json'; import nodeTestDistRc from './data/node-rc-index.json'; import nodeV8CanaryTestDist from './data/v8-canary-dist-index.json'; +import canaryBuild from '../src/distributions/v8-canary/canary_builds'; describe('setup-node', () => { let inputs = {} as any; @@ -528,4 +529,159 @@ describe('setup-node', () => { expect(cacheSpy).not.toHaveBeenCalled(); }); }); + + describe('CanaryBuild - Mirror URL functionality', () => { + class CanaryBuild { + mirrorURL: string | undefined; + nodeInfo: NodeInputs; + + constructor(nodeInfo: NodeInputs) { + this.nodeInfo = nodeInfo; // Store the nodeInfo object passed into the constructor + this.mirrorURL = nodeInfo.mirrorURL; // Set mirrorURL from nodeInfo, or undefined if not provided + } + + async getDistributionMirrorUrl() { + // Check if mirror URL is undefined or empty, and return the default if so + if (!this.mirrorURL) { + core.info('Using mirror URL: https://nodejs.org/download/v8-canary'); + return 'https://nodejs.org/download/v8-canary'; // Default URL + } else { + if (this.mirrorURL === '') { + throw new Error( + 'Mirror URL is empty. Please provide a valid mirror URL.' + ); + } + return this.mirrorURL; + } + } + } + + it('should use the mirror URL from nodeInfo if provided', () => { + // Mocking core.info to track the log calls + const infoSpy = jest.spyOn(core, 'info').mockImplementation(() => {}); + + const mirrorURL = 'https://custom.mirror.url/v8-canary'; + const nodeInfo: NodeInputs = { + versionSpec: '8.0.0-canary', + arch: 'x64', + checkLatest: false, + stable: false, + mirrorURL: mirrorURL // Provide the custom mirror URL + }; + + const canaryBuild = new CanaryBuild(nodeInfo); + + // Call the method to get the mirror URL + const distributionMirrorUrl = canaryBuild.getDistributionMirrorUrl(); + + // Assert that core.info was called with the custom mirror URL + expect(infoSpy).toHaveBeenCalledWith(`Using mirror URL: ${mirrorURL}`); + + // Assert that the returned URL is the custom mirror URL + expect(distributionMirrorUrl).toBe(mirrorURL); + + // Restore the original core.info implementation + infoSpy.mockRestore(); + }); + it('should fall back to the default distribution URL if mirror URL is not provided', () => { + const infoSpy = jest.spyOn(core, 'info').mockImplementation(() => {}); + + const nodeInfo: NodeInputs = { + versionSpec: '8.0.0-canary', + arch: 'x64', + checkLatest: false, + stable: false + // No mirrorURL provided here + }; + + const canaryBuild = new CanaryBuild(nodeInfo); + + // Call the method to get the distribution URL + const distributionMirrorUrl = canaryBuild.getDistributionMirrorUrl(); + + // Assert that core.info was called with the default URL + expect(infoSpy).toHaveBeenCalledWith( + 'Using mirror URL: https://nodejs.org/download/v8-canary' + ); + + // Assert that the returned URL is the default one + expect(distributionMirrorUrl).toBe( + 'https://nodejs.org/download/v8-canary' + ); + + infoSpy.mockRestore(); + }); + + it('should log the correct info when mirror URL is not provided', () => { + const infoSpy = jest.spyOn(core, 'info').mockImplementation(() => {}); + + const nodeInfo: NodeInputs = { + versionSpec: '8.0.0-canary', + arch: 'x64', + checkLatest: false, + stable: false + // No mirrorURL provided here + }; + + const canaryBuild = new CanaryBuild(nodeInfo); + + // Call the method + canaryBuild.getDistributionMirrorUrl(); + + // Assert that core.info was called with the fallback URL + expect(infoSpy).toHaveBeenCalledWith( + 'Using mirror URL: https://nodejs.org/download/v8-canary' + ); + + infoSpy.mockRestore(); + }); + + it('should return mirror URL if provided in nodeInfo', () => { + // Custom mirror URL to be tested + const mirrorURL = 'https://custom.mirror.url/v8-canary'; + + // Create a spy on core.info to track its calls + const infoSpy = jest.spyOn(core, 'info').mockImplementation(() => {}); // Mocking core.info + + // Prepare the nodeInfo object with the custom mirror URL + const nodeInfo: NodeInputs = { + versionSpec: '8.0.0-canary', + arch: 'x64', + mirrorURL: mirrorURL, // Custom mirrorURL provided + checkLatest: false, + stable: false + }; + + // Create an instance of CanaryBuild, passing nodeInfo to the constructor + const canaryBuild = new CanaryBuild(nodeInfo); + + // Call the method + const distributionMirrorUrl = canaryBuild.getDistributionMirrorUrl(); + + // Assert that core.info was called with the expected message + expect(infoSpy).toHaveBeenCalledWith(`Using mirror URL: ${mirrorURL}`); + + // Assert that the returned mirror URL is correct + expect(distributionMirrorUrl).toBe(mirrorURL); + + // Restore the original core.info function after the test + infoSpy.mockRestore(); + }); + it('should throw an error if mirror URL is empty string', async () => { + const nodeInfo: NodeInputs = { + versionSpec: '8.0.0-canary', + arch: 'x64', + checkLatest: false, + stable: false, + mirrorURL: '' // Empty string provided as mirror URL + }; + + const canaryBuild = new CanaryBuild(nodeInfo); + + // Expect the method to throw an error for empty string mirror URL + expect(canaryBuild.getDistributionMirrorUrl()).toThrow( + 'Mirror URL is empty. Please provide a valid mirror URL.' + ); + }); + }); }); diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 501741a6..7487cf7a 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,4 +1,5 @@ import * as core from '@actions/core'; +import 'jest'; import * as exec from '@actions/exec'; import * as tc from '@actions/tool-cache'; import * as cache from '@actions/cache'; @@ -14,6 +15,11 @@ import * as main from '../src/main'; import * as util from '../src/util'; import OfficialBuilds from '../src/distributions/official_builds/official_builds'; +import * as installerFactory from '../src/distributions/installer-factory'; +jest.mock('../src/distributions/installer-factory', () => ({ + getNodejsDistribution: jest.fn() +})); + describe('main tests', () => { let inputs = {} as any; let os = {} as any; @@ -280,4 +286,93 @@ describe('main tests', () => { ); }); }); + + // Create a mock object that satisfies the BaseDistribution interface + const createMockNodejsDistribution = () => ({ + setupNodeJs: jest.fn(), + httpClient: {}, // Mocking the httpClient (you can replace this with more detailed mocks if needed) + osPlat: 'darwin', // Mocking osPlat (the platform the action will run on, e.g., 'darwin', 'win32', 'linux') + nodeInfo: { + version: '14.x', + arch: 'x64', + platform: 'darwin' + }, + getDistributionUrl: jest.fn().mockReturnValue('https://nodejs.org/dist/'), // Example URL + install: jest.fn(), + validate: jest.fn() + // Add any other methods/properties required by your BaseDistribution type + }); + + describe('Mirror URL Tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should pass mirror URL correctly when provided', async () => { + jest.spyOn(core, 'getInput').mockImplementation((name: string) => { + if (name === 'mirror-url') return 'https://custom-mirror-url.com'; + if (name === 'node-version') return '14.x'; + return ''; + }); + + const mockNodejsDistribution = createMockNodejsDistribution(); + (installerFactory.getNodejsDistribution as jest.Mock).mockReturnValue( + mockNodejsDistribution + ); + + await main.run(); + + // Ensure setupNodeJs is called with the correct parameters, including the mirror URL + expect(mockNodejsDistribution.setupNodeJs).toHaveBeenCalledWith({ + versionSpec: '14.x', + checkLatest: false, + auth: undefined, + stable: true, + arch: 'x64', + mirrorURL: 'https://custom-mirror-url.com' // Ensure this matches + }); + }); + + it('should use default mirror URL when no mirror URL is provided', async () => { + jest.spyOn(core, 'getInput').mockImplementation((name: string) => { + if (name === 'mirror-url') return ''; // Simulating no mirror URL provided + if (name === 'node-version') return '14.x'; + return ''; + }); + + const mockNodejsDistribution = createMockNodejsDistribution(); + (installerFactory.getNodejsDistribution as jest.Mock).mockReturnValue( + mockNodejsDistribution + ); + + await main.run(); + + // Expect that setupNodeJs is called with an empty mirror URL (default behavior) + expect(mockNodejsDistribution.setupNodeJs).toHaveBeenCalledWith( + expect.objectContaining({ + mirrorURL: '' // Default URL is expected to be handled internally + }) + ); + }); + + it('should handle mirror URL with spaces correctly', async () => { + const mirrorURL = 'https://custom-mirror-url.com '; + const expectedTrimmedURL = 'https://custom-mirror-url.com'; + + // Mock the setupNodeJs function + const mockNodejsDistribution = { + setupNodeJs: jest.fn() + }; + + // Simulate calling the main function that will trigger setupNodeJs + await main.run(); + + // Assert that setupNodeJs was called with the correct trimmed mirrorURL + expect(mockNodejsDistribution.setupNodeJs).toHaveBeenCalledWith( + expect.objectContaining({ + mirrorURL: expectedTrimmedURL // Ensure the URL is trimmed properly + }) + ); + }); + }); }); diff --git a/__tests__/nightly-installer.test.ts b/__tests__/nightly-installer.test.ts index 87c43795..c794e8df 100644 --- a/__tests__/nightly-installer.test.ts +++ b/__tests__/nightly-installer.test.ts @@ -10,8 +10,8 @@ import osm from 'os'; import path from 'path'; import * as main from '../src/main'; import * as auth from '../src/authutil'; -import {INodeVersion} from '../src/distributions/base-models'; - +import {INodeVersion, NodeInputs} from '../src/distributions/base-models'; +import NightlyNodejs from '../src/distributions/nightly/nightly_builds'; import nodeTestManifest from './data/versions-manifest.json'; import nodeTestDist from './data/node-dist-index.json'; import nodeTestDistNightly from './data/node-nightly-index.json'; @@ -606,3 +606,139 @@ describe('setup-node', () => { ); }); }); +// Mock core.info to track the log output +jest.mock('@actions/core', () => ({ + info: jest.fn() +})); + +// Create a subclass to access the protected method for testing purposes +class TestNightlyNodejs extends NightlyNodejs { + nodeInputs: NodeInputs; + + constructor(nodeInputs: NodeInputs) { + super(nodeInputs); + this.nodeInputs = nodeInputs; + } + + getDistributionUrlPublic() { + // If a mirrorURL is provided, return it; otherwise, return the default URL + if (this.nodeInputs.mirrorURL && this.nodeInputs.mirrorURL.trim() !== '') { + core.info(`Using mirror URL: ${this.nodeInputs.mirrorURL}`); + return this.nodeInputs.mirrorURL; + } else { + core.info('Using default distribution URL for nightly Node.js.'); + return 'https://nodejs.org/download/nightly'; + } + } +} +describe('NightlyNodejs', () => { + it('uses mirror URL when provided', async () => { + const mirrorURL = 'https://my.custom.mirror/nodejs/nightly'; + const nodeInfo: NodeInputs = { + mirrorURL: mirrorURL, // Use the custom mirror URL here + versionSpec: '18.0.0-nightly', + arch: 'x64', + checkLatest: false, + stable: false + }; + + const nightlyNode = new TestNightlyNodejs(nodeInfo); + + const distributionUrl = nightlyNode.getDistributionUrlPublic(); + + // Check if the correct distribution URL is being used + expect(distributionUrl).toBe(mirrorURL); + + // Verify if the core.info was called with the correct message + expect(core.info).toHaveBeenCalledWith(`Using mirror URL: ${mirrorURL}`); + }); + + it('falls back to default distribution URL when no mirror URL is provided', async () => { + const nodeInfo: NodeInputs = { + versionSpec: '18.0.0-nightly', + arch: 'x64', + checkLatest: false, + stable: false + }; + const nightlyNode = new TestNightlyNodejs(nodeInfo); + + const distributionUrl = nightlyNode.getDistributionUrlPublic(); + + expect(distributionUrl).toBe('https://nodejs.org/download/nightly'); + expect(core.info).toHaveBeenCalledWith( + 'Using default distribution URL for nightly Node.js.' + ); + }); + + const core = require('@actions/core'); // Mock core + jest.spyOn(core, 'info').mockImplementation(() => {}); // Mock core.info function + + it('logs mirror URL when provided', async () => { + const mirrorURL = 'https://custom.mirror/nodejs/nightly'; + + const nodeInfo = { + mirrorURL: mirrorURL, // Set the mirror URL correctly + versionSpec: '18.0.0-nightly', + arch: 'x64', + checkLatest: false, + stable: false + }; + + const nightlyNode = new TestNightlyNodejs(nodeInfo); + await nightlyNode.getDistributionUrlPublic(); // Ensure to await if the function is async + + expect(core.info).toHaveBeenCalledWith(`Using mirror URL: ${mirrorURL}`); + }); + + it('logs default URL when no mirror URL is provided', async () => { + const nodeInfo: NodeInputs = { + versionSpec: '18.0.0-nightly', + arch: 'x64', + checkLatest: false, + stable: false + }; + const nightlyNode = new TestNightlyNodejs(nodeInfo); + + nightlyNode.getDistributionUrlPublic(); + + expect(core.info).toHaveBeenCalledWith( + 'Using default distribution URL for nightly Node.js.' + ); + }); + + it('falls back to default distribution URL if mirror URL is an empty string', async () => { + const nodeInfo: NodeInputs = { + mirrorURL: '', + versionSpec: '18.0.0-nightly', + arch: 'x64', + checkLatest: false, + stable: false + }; + const nightlyNode = new TestNightlyNodejs(nodeInfo); + + const distributionUrl = nightlyNode.getDistributionUrlPublic(); + + expect(distributionUrl).toBe('https://nodejs.org/download/nightly'); + expect(core.info).toHaveBeenCalledWith( + 'Using default distribution URL for nightly Node.js.' + ); + }); + + it('falls back to default distribution URL if mirror URL is undefined', async () => { + const nodeInfo: NodeInputs = { + mirrorURL: '', + versionSpec: '18.0.0-nightly', + arch: 'x64', + checkLatest: false, + stable: false + }; + const nightlyNode = new TestNightlyNodejs(nodeInfo); + + const distributionUrl = nightlyNode.getDistributionUrlPublic(); + + expect(distributionUrl).toBe('https://nodejs.org/download/nightly'); + expect(core.info).toHaveBeenCalledWith( + 'Using default distribution URL for nightly Node.js.' + ); + }); +}); diff --git a/__tests__/official-installer.test.ts b/__tests__/official-installer.test.ts index 2d8f17cf..c7d90dda 100644 --- a/__tests__/official-installer.test.ts +++ b/__tests__/official-installer.test.ts @@ -11,7 +11,7 @@ import path from 'path'; import * as main from '../src/main'; import * as auth from '../src/authutil'; import OfficialBuilds from '../src/distributions/official_builds/official_builds'; -import {INodeVersion} from '../src/distributions/base-models'; +import {INodeVersion, NodeInputs} from '../src/distributions/base-models'; import nodeTestManifest from './data/versions-manifest.json'; import nodeTestDist from './data/node-dist-index.json'; @@ -828,4 +828,144 @@ describe('setup-node', () => { } ); }); + + import {OfficialBuilds} from './path-to-your-official-builds-file'; // Adjust path + import * as core from '@actions/core'; + import * as tc from '@actions/tool-cache'; + + jest.mock('@actions/core'); + jest.mock('@actions/tool-cache'); + + describe('OfficialBuilds - Mirror URL functionality', () => { + let officialBuilds: OfficialBuilds; + + beforeEach(() => { + const mockNodeInfo = { + versionSpec: '16.x', + mirrorURL: 'https://my.custom.mirror/nodejs', + arch: 'x64', + stable: true, + checkLatest: false, + osPlat: 'linux', // Mock OS platform to avoid "undefined" error + auth: 'someAuthToken' + }; + officialBuilds = new OfficialBuilds(mockNodeInfo); + }); + + it('should download using the mirror URL when provided', async () => { + const mockDownloadPath = '/some/temp/path'; + const mockDownloadTool = jest + .spyOn(tc, 'downloadTool') + .mockResolvedValue(mockDownloadPath); + const mockAddPath = jest + .spyOn(core, 'addPath') + .mockImplementation(() => {}); + + await officialBuilds.setupNodeJs(); + + // Check if the mirror URL was used + expect(core.info).toHaveBeenCalledWith( + 'Attempting to download using mirror URL...' + ); + expect(core.info).toHaveBeenCalledWith( + 'downloadPath from downloadFromMirrorURL() /some/temp/path' + ); + expect(core.addPath).toHaveBeenCalledWith(mockDownloadPath); + }); + + it('should log a message when mirror URL is used', async () => { + const mockInfo = jest.spyOn(core, 'info').mockImplementation(() => {}); + + await officialBuilds.setupNodeJs(); + + // Check if the appropriate message is logged for mirror URL + expect(core.info).toHaveBeenCalledWith( + `Using mirror URL: https://my.custom.mirror/nodejs` + ); + }); + + it('should fall back to default URL if mirror URL is not provided', async () => { + // Mock a scenario where mirror URL is not provided + officialBuilds.nodeInfo.mirrorURL = undefined; + + const mockInfo = jest.spyOn(core, 'info').mockImplementation(() => {}); + + await officialBuilds.setupNodeJs(); + + // Check if fallback logic was triggered + expect(core.info).toHaveBeenCalledWith( + 'Falling back to download directly from Node' + ); + }); + + it('should log an error and handle failure during mirror URL download', async () => { + const errorMessage = 'Network error'; + const mockError = jest.spyOn(core, 'error').mockImplementation(() => {}); + const mockDebug = jest.spyOn(core, 'debug').mockImplementation(() => {}); + + const mockDownloadTool = jest + .spyOn(tc, 'downloadTool') + .mockRejectedValue(new Error(errorMessage)); + + try { + await officialBuilds.setupNodeJs(); + } catch (error) { + // Expect core.error to be called with the error message + expect(core.error).toHaveBeenCalledWith(errorMessage); + expect(core.debug).toHaveBeenCalledWith( + expect.stringContaining('empty stack') + ); + } + }); + + it('should log a fallback message if downloading from the mirror URL fails', async () => { + const mockInfo = jest.spyOn(core, 'info').mockImplementation(() => {}); + const mockDownloadTool = jest + .spyOn(tc, 'downloadTool') + .mockRejectedValue(new Error('Download failed')); + + await officialBuilds.setupNodeJs(); + + // Check if fallback log message was triggered + expect(core.info).toHaveBeenCalledWith( + 'Failed to download from mirror URL. Falling back to default Node.js URL...' + ); + }); + + it('should throw an error if mirror URL is not provided and downloading from both mirror and default fails', async () => { + const errorMessage = `Unable to find Node version for platform linux and architecture x64.`; + + const mockDownloadTool = jest + .spyOn(tc, 'downloadTool') + .mockRejectedValue(new Error('Download failed')); + const mockGetNodeJsVersions = jest + .spyOn(officialBuilds, 'getNodeJsVersions') + .mockResolvedValue([]); + + // Simulating failure in getting versions and download + try { + await officialBuilds.setupNodeJs(); + } catch (error) { + expect(error.message).toContain(errorMessage); + } + }); + + it('should throw an error if mirror URL is undefined and not provided', async () => { + const errorMessage = `Unable to find Node version for platform linux and architecture x64.`; + officialBuilds.nodeInfo.mirrorURL = undefined; // Simulate missing mirror URL + + const mockGetNodeJsVersions = jest + .spyOn(officialBuilds, 'getNodeJsVersions') + .mockResolvedValue([]); + const mockDownloadTool = jest + .spyOn(tc, 'downloadTool') + .mockRejectedValue(new Error('Download failed')); + + try { + await officialBuilds.setupNodeJs(); + } catch (error) { + expect(error.message).toContain(errorMessage); + } + }); + }); }); diff --git a/__tests__/rc-installer.test.ts b/__tests__/rc-installer.test.ts index 736260a4..2f72b96d 100644 --- a/__tests__/rc-installer.test.ts +++ b/__tests__/rc-installer.test.ts @@ -10,12 +10,13 @@ import osm from 'os'; import path from 'path'; import * as main from '../src/main'; import * as auth from '../src/authutil'; -import {INodeVersion} from '../src/distributions/base-models'; +import {INodeVersion, NodeInputs} from '../src/distributions/base-models'; import nodeTestDist from './data/node-dist-index.json'; import nodeTestDistNightly from './data/node-nightly-index.json'; import nodeTestDistRc from './data/node-rc-index.json'; import nodeV8CanaryTestDist from './data/v8-canary-dist-index.json'; +import RcBuild from '../src/distributions/rc/rc_builds'; describe('setup-node', () => { let inputs = {} as any; @@ -144,6 +145,10 @@ describe('setup-node', () => { const toolPath = path.normalize('/cache/node/12.0.0-rc.1/x64'); findSpy.mockImplementation(() => toolPath); + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + const cnSpy = jest.spyOn(process.stdout, 'write'); // Ensure this spies on the correct add-path function + await main.run(); expect(logSpy).toHaveBeenCalledWith(`Found in cache @ ${toolPath}`); @@ -156,6 +161,10 @@ describe('setup-node', () => { const toolPath = path.normalize('/cache/node/12.0.0-rc.1/x64'); findSpy.mockImplementation(() => toolPath); + + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + await main.run(); expect(logSpy).toHaveBeenCalledWith(`Found in cache @ ${toolPath}`); @@ -168,6 +177,10 @@ describe('setup-node', () => { const toolPath = path.normalize('/cache/node/12.0.0-rc.1/x64'); findSpy.mockImplementation(() => toolPath); + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + const cnSpy = jest.spyOn(process.stdout, 'write'); // Ensure this spies on the correct add-path function + await main.run(); const expPath = path.join(toolPath, 'bin'); @@ -224,6 +237,10 @@ describe('setup-node', () => { inputs['node-version'] = versionSpec; findSpy.mockImplementation(() => ''); + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + const cnSpy = jest.spyOn(process.stdout, 'write'); // Ensure this spies on the correct add-path function + await main.run(); expect(cnSpy).toHaveBeenCalledWith( @@ -247,6 +264,11 @@ describe('setup-node', () => { dlSpy.mockImplementation(() => { throw new Error(errMsg); }); + + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + const cnSpy = jest.spyOn(process.stdout, 'write'); // Ensure this spies on the correct add-path function + await main.run(); expect(cnSpy).toHaveBeenCalledWith(`::error::${errMsg}${osm.EOL}`); @@ -281,6 +303,9 @@ describe('setup-node', () => { const toolPath = path.normalize(`/cache/node/${version}/${arch}`); exSpy.mockImplementation(async () => '/some/other/temp/path'); cacheSpy.mockImplementation(async () => toolPath); + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + const cnSpy = jest.spyOn(process.stdout, 'write'); // Ensure this spies on the correct add-path function await main.run(); expect(dlSpy).toHaveBeenCalled(); @@ -331,6 +356,11 @@ describe('setup-node', () => { inputs['node-version'] = input; os['arch'] = 'x64'; os['platform'] = 'linux'; + + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + const cnSpy = jest.spyOn(process.stdout, 'write'); // Ensure this spies on the correct add-path function + // act await main.run(); @@ -352,14 +382,18 @@ describe('setup-node', () => { 'finds the %s version in the hostedToolcache', async (input, expectedVersion) => { const toolPath = path.normalize(`/cache/node/${expectedVersion}/x64`); - findSpy.mockImplementation((_, version) => - path.normalize(`/cache/node/${version}/x64`) - ); + + // Mocking the behavior of findSpy and findAllVersionsSpy + findSpy.mockImplementation((_, version) => { + console.log(`findSpy called for version: ${version}`); // Debugging line + return path.normalize(`/cache/node/${version}/x64`); + }); + findAllVersionsSpy.mockReturnValue([ '2.2.2-rc.2', '1.1.1-rc.1', '99.1.1', - expectedVersion, + expectedVersion, // This should be the expected version '88.1.1', '3.3.3-rc.3' ]); @@ -368,14 +402,27 @@ describe('setup-node', () => { os['arch'] = 'x64'; os['platform'] = 'linux'; - // act + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + const cnSpy = jest.spyOn(process.stdout, 'write'); // Ensure this spies on the correct add-path function + + // Act: Run the main function (your application logic) await main.run(); - // assert + // Debugging output to check if logSpy was called + console.log('logSpy calls:', logSpy.mock.calls); // Debugging line + + // Assert: Check that the logSpy was called with the correct message expect(logSpy).toHaveBeenCalledWith(`Found in cache @ ${toolPath}`); + + // Assert: Check that cnSpy was called with the correct add-path action expect(cnSpy).toHaveBeenCalledWith( `::add-path::${path.join(toolPath, 'bin')}${osm.EOL}` ); + + // Clean up spies + logSpy.mockRestore(); + cnSpy.mockRestore(); } ); @@ -390,6 +437,10 @@ describe('setup-node', () => { inputs['node-version'] = versionSpec; os['arch'] = 'x64'; os['platform'] = 'linux'; + // Ensure spies are set up before running the main logic + const logSpy = jest.spyOn(console, 'log'); // Ensure this is spying on console.log + const cnSpy = jest.spyOn(process.stdout, 'write'); // Ensure this spies on the correct add-path function + // act await main.run(); @@ -399,4 +450,124 @@ describe('setup-node', () => { ); }); }); + + describe('RcBuild - Mirror URL functionality', () => { + const nodeInfo: NodeInputs = { + versionSpec: '18.0.0-rc', + arch: 'x64', + mirrorURL: '', + checkLatest: false, + stable: false + }; + + class RcBuild { + mirrorURL: string | undefined; + nodeInfo: NodeInputs; + + constructor(nodeInfo: NodeInputs) { + this.nodeInfo = nodeInfo; // Store the nodeInfo object passed into the constructor + this.mirrorURL = nodeInfo.mirrorURL; // Set mirrorURL from nodeInfo, or undefined if not provided + } + + getDistributionMirrorUrl() { + // If mirrorURL is provided in nodeInfo, return it + if (this.nodeInfo.mirrorURL != '') { + core.info(`Using mirror URL: ${this.nodeInfo.mirrorURL}`); + return this.nodeInfo.mirrorURL; + } else { + if (this.nodeInfo.mirrorURL === '') { + throw new Error( + 'Mirror URL is empty. Please provide a valid mirror URL.' + ); + } else { + if (this.nodeInfo.mirrorURL === undefined) { + throw new Error( + 'Mirror URL is undefined. Please provide a valid mirror URL.' + ); + } + } + } + } + } + + it('should return the default distribution URL if no mirror URL is provided', () => { + // Assuming nodeInfo does not have a mirrorURL + const nodeInfo = { + versionSpec: '16.0.0-rc', + arch: 'x64', + checkLatest: false, + stable: false, + mirrorURL: '' // No mirror URL provided + }; + + const rcBuild = new RcBuild(nodeInfo); + + const distributionUrl = rcBuild.getDistributionMirrorUrl(); + + // Default URL + expect(distributionUrl).toBe('https://nodejs.org/download/rc'); + }); + + it('should use the mirror URL from nodeInfo if provided', () => { + const mirrorURL = 'https://my.custom.mirror/nodejs'; // Set the custom mirror URL + nodeInfo.mirrorURL = mirrorURL; // Set the mirrorURL in nodeInfo + + const rcBuild = new RcBuild(nodeInfo); + + // Mock core.info to track its calls + const infoSpy = jest.spyOn(core, 'info').mockImplementation(() => {}); + + // Call the method + const distributionMirrorUrl = rcBuild.getDistributionMirrorUrl(); // Access the method + + // Assert that core.info was called with the correct mirror URL message + expect(infoSpy).toHaveBeenCalledWith(`Using mirror URL: ${mirrorURL}`); + + // Assert that the returned URL is the mirror URL + expect(distributionMirrorUrl).toBe(mirrorURL); + + // Restore the original core.info function after the test + infoSpy.mockRestore(); + }); + + it('should throw an error if mirror URL is empty', () => { + nodeInfo.mirrorURL = ''; // Empty mirror URL + + const rcBuild = new RcBuild(nodeInfo); + + // Mock core.info to track its calls + const infoSpy = jest.spyOn(core, 'info').mockImplementation(() => {}); + + // Expect the function to return the default URL because the mirror URL is empty + const distributionMirrorUrl = rcBuild.getDistributionMirrorUrl(); + + // Assert the returned URL is the default URL + expect(distributionMirrorUrl).toBe('https://nodejs.org/download/rc'); + + // Ensure that core.info was NOT called because it's not a custom mirror URL + expect(infoSpy).not.toHaveBeenCalled(); + + // Restore the original core.info function after the test + infoSpy.mockRestore(); + }); + + it('should throw an error if mirror URL is undefined', () => { + nodeInfo.mirrorURL = undefined; // Undefined mirror URL + + const rcBuild = new RcBuild(nodeInfo); + + // Mock core.info to track its calls + const infoSpy = jest.spyOn(core, 'info').mockImplementation(() => {}); + + // Expect the function to throw an error due to undefined mirror URL + expect(() => rcBuild.getDistributionMirrorUrl()).toThrowError( + 'Mirror URL is undefined. Please provide a valid mirror URL.' + ); + + // Ensure that core.info was NOT called because it's not a valid URL + expect(infoSpy).not.toHaveBeenCalled(); + + infoSpy.mockRestore(); + }); + }); }); diff --git a/dist/setup/index.js b/dist/setup/index.js index cdca1dbf..943b9007 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -100154,6 +100154,32 @@ class BaseDistribution { return response.result || []; }); } + getMirrorUrlVersions() { + return __awaiter(this, void 0, void 0, function* () { + const initialUrl = this.getDistributionUrl(); + const dataUrl = `${initialUrl}/index.json`; + try { + const response = yield this.httpClient.getJson(dataUrl); + return response.result || []; + } + catch (err) { + if (err instanceof Error && + err.message.includes('getaddrinfo EAI_AGAIN')) { + core.error(`Network error: Failed to resolve the server at ${dataUrl}. + Please check your DNS settings or verify that the URL is correct.`); + } + else if (err instanceof hc.HttpClientError && err.statusCode === 404) { + core.error(`404 Error: Unable to find versions at ${dataUrl}. + Please verify that the mirror URL is valid.`); + } + else { + core.error(`Failed to fetch Node.js versions from ${dataUrl}. + Please check the URL and try again.}`); + } + throw err; + } + }); + } getNodejsDistInfo(version) { const osArch = this.translateArchToDistUrl(this.nodeInfo.arch); version = semver_1.default.clean(version) || ''; @@ -100174,6 +100200,26 @@ class BaseDistribution { fileName: fileName }; } + getNodejsMirrorURLInfo(version) { + const mirrorURL = this.nodeInfo.mirrorURL; + const osArch = this.translateArchToDistUrl(this.nodeInfo.arch); + version = semver_1.default.clean(version) || ''; + const fileName = this.osPlat == 'win32' + ? `node-v${version}-win-${osArch}` + : `node-v${version}-${this.osPlat}-${osArch}`; + const urlFileName = this.osPlat == 'win32' + ? this.nodeInfo.arch === 'arm64' + ? `${fileName}.zip` + : `${fileName}.7z` + : `${fileName}.tar.gz`; + const url = `${mirrorURL}/v${version}/${urlFileName}`; + return { + downloadUrl: url, + resolvedVersion: version, + arch: osArch, + fileName: fileName + }; + } downloadNodejs(info) { return __awaiter(this, void 0, void 0, function* () { let downloadPath = ''; @@ -100185,8 +100231,15 @@ class BaseDistribution { if (err instanceof tc.HTTPError && err.httpStatusCode == 404 && this.osPlat == 'win32') { - return yield this.acquireWindowsNodeFromFallbackLocation(info.resolvedVersion, info.arch); + return yield this.acquireWindowsNodeFromFallbackLocation(info.resolvedVersion, info.arch, info.downloadUrl); } + // Handle network-related issues (e.g., DNS resolution failures) + if (err instanceof Error && + err.message.includes('getaddrinfo EAI_AGAIN')) { + core.error(`Network error: Failed to resolve the server at ${info.downloadUrl}. + This could be due to a DNS resolution issue. Please verify the URL or check your network connection.`); + } + core.error(`Download failed from ${info.downloadUrl}. Please check the URl and try again.`); throw err; } const toolPath = yield this.extractArchive(downloadPath, info, true); @@ -100202,8 +100255,9 @@ class BaseDistribution { return { range: valid, options }; } acquireWindowsNodeFromFallbackLocation(version_1) { - return __awaiter(this, arguments, void 0, function* (version, arch = os_1.default.arch()) { + return __awaiter(this, arguments, void 0, function* (version, arch = os_1.default.arch(), downloadUrl) { const initialUrl = this.getDistributionUrl(); + core.info('url: ' + initialUrl); const osArch = this.translateArchToDistUrl(arch); // Create temporary folder to download to const tempDownloadFolder = `temp_${(0, uuid_1.v4)()}`; @@ -100217,6 +100271,9 @@ 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}`); + if (downloadUrl != exeUrl) { + core.error('unable to download node binary with the provided URL. Please check and try again'); + } const exePath = yield tc.downloadTool(exeUrl); yield io.cp(exePath, path.join(tempDir, 'node.exe')); const libPath = yield tc.downloadTool(libUrl); @@ -100392,7 +100449,22 @@ class NightlyNodejs extends base_distribution_prerelease_1.default { this.distribution = 'nightly'; } getDistributionUrl() { - return 'https://nodejs.org/download/nightly'; + if (this.nodeInfo.mirrorURL) { + if (this.nodeInfo.mirrorURL != '') { + return this.nodeInfo.mirrorURL; + } + else { + if (this.nodeInfo.mirrorURL === '') { + throw new Error('Mirror URL is empty. Please provide a valid mirror URL.'); + } + else { + throw new Error('Mirror URL is not a valid'); + } + } + } + else { + return 'https://nodejs.org/download/nightly'; + } } } exports["default"] = NightlyNodejs; @@ -100451,73 +100523,94 @@ class OfficialBuilds extends base_distribution_1.default { } setupNodeJs() { return __awaiter(this, void 0, void 0, function* () { - var _a; - let manifest; - let nodeJsVersions; - const osArch = this.translateArchToDistUrl(this.nodeInfo.arch); - if (this.isLtsAlias(this.nodeInfo.versionSpec)) { - core.info('Attempt to resolve LTS alias from manifest...'); - // No try-catch since it's not possible to resolve LTS alias without manifest - manifest = yield this.getManifest(); - this.nodeInfo.versionSpec = this.resolveLtsAliasFromManifest(this.nodeInfo.versionSpec, this.nodeInfo.stable, manifest); - } - if (this.isLatestSyntax(this.nodeInfo.versionSpec)) { - nodeJsVersions = yield this.getNodeJsVersions(); - const versions = this.filterVersions(nodeJsVersions); - this.nodeInfo.versionSpec = this.evaluateVersions(versions); - core.info('getting latest node version...'); - } - if (this.nodeInfo.checkLatest) { - core.info('Attempt to resolve the latest version from manifest...'); - const resolvedVersion = yield this.resolveVersionFromManifest(this.nodeInfo.versionSpec, this.nodeInfo.stable, osArch, manifest); - if (resolvedVersion) { - this.nodeInfo.versionSpec = resolvedVersion; - core.info(`Resolved as '${resolvedVersion}'`); + var _a, _b; + if (this.nodeInfo.mirrorURL) { + if (this.nodeInfo.mirrorURL === '') { + throw new Error('Mirror URL is empty. Please provide a valid mirror URL.'); } - else { - core.info(`Failed to resolve version ${this.nodeInfo.versionSpec} from manifest`); - } - } - let toolPath = this.findVersionInHostedToolCacheDirectory(); - if (toolPath) { - core.info(`Found in cache @ ${toolPath}`); - this.addToolPath(toolPath); - return; - } - let downloadPath = ''; - try { - core.info(`Attempting to download ${this.nodeInfo.versionSpec}...`); - 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); + let downloadPath = ''; + let toolPath = ''; + try { + core.info(`Attempting to download using mirror URL...`); + downloadPath = yield this.downloadFromMirrorURL(); // Attempt to download from the mirror + core.info('downloadPath from downloadFromMirrorURL() ' + downloadPath); if (downloadPath) { - toolPath = yield this.extractArchive(downloadPath, versionInfo, false); + toolPath = downloadPath; } } - else { - core.info('Not found in manifest. Falling back to download directly from Node'); - } - } - catch (err) { - // Rate limit? - if (err instanceof tc.HTTPError && - (err.httpStatusCode === 403 || err.httpStatusCode === 429)) { - core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); - } - else { + catch (err) { core.info(err.message); + core.debug((_a = err.stack) !== null && _a !== void 0 ? _a : 'empty stack'); } - core.debug((_a = err.stack) !== null && _a !== void 0 ? _a : 'empty stack'); - core.info('Falling back to download directly from Node'); } - if (!toolPath) { - toolPath = yield this.downloadDirectlyFromNode(); + else { + let manifest; + let nodeJsVersions; + const osArch = this.translateArchToDistUrl(this.nodeInfo.arch); + if (this.isLtsAlias(this.nodeInfo.versionSpec)) { + core.info('Attempt to resolve LTS alias from manifest...'); + // No try-catch since it's not possible to resolve LTS alias without manifest + manifest = yield this.getManifest(); + this.nodeInfo.versionSpec = this.resolveLtsAliasFromManifest(this.nodeInfo.versionSpec, this.nodeInfo.stable, manifest); + } + if (this.isLatestSyntax(this.nodeInfo.versionSpec)) { + nodeJsVersions = yield this.getNodeJsVersions(); + const versions = this.filterVersions(nodeJsVersions); + this.nodeInfo.versionSpec = this.evaluateVersions(versions); + core.info('getting latest node version...'); + } + if (this.nodeInfo.checkLatest) { + core.info('Attempt to resolve the latest version from manifest...'); + const resolvedVersion = yield this.resolveVersionFromManifest(this.nodeInfo.versionSpec, this.nodeInfo.stable, osArch, manifest); + if (resolvedVersion) { + this.nodeInfo.versionSpec = resolvedVersion; + core.info(`Resolved as '${resolvedVersion}'`); + } + else { + core.info(`Failed to resolve version ${this.nodeInfo.versionSpec} from manifest`); + } + } + let toolPath = this.findVersionInHostedToolCacheDirectory(); + if (toolPath) { + core.info(`Found in cache @ ${toolPath}`); + this.addToolPath(toolPath); + return; + } + let downloadPath = ''; + try { + core.info(`Attempting to download ${this.nodeInfo.versionSpec}...`); + 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); + if (downloadPath) { + toolPath = yield this.extractArchive(downloadPath, versionInfo, false); + } + } + else { + core.info('Not found in manifest. Falling back to download directly from Node'); + } + } + catch (err) { + // Rate limit? + if (err instanceof tc.HTTPError && + (err.httpStatusCode === 403 || err.httpStatusCode === 429)) { + core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); + } + else { + core.info(err.message); + } + core.debug((_b = err.stack) !== null && _b !== void 0 ? _b : 'empty stack'); + core.info('Falling back to download directly from Node'); + } + if (!toolPath) { + toolPath = yield this.downloadDirectlyFromNode(); + } + if (this.osPlat != 'win32') { + toolPath = path_1.default.join(toolPath, 'bin'); + } + core.addPath(toolPath); } - if (this.osPlat != 'win32') { - toolPath = path_1.default.join(toolPath, 'bin'); - } - core.addPath(toolPath); }); } addToolPath(toolPath) { @@ -100559,6 +100652,9 @@ class OfficialBuilds extends base_distribution_1.default { return version; } getDistributionUrl() { + if (this.nodeInfo.mirrorURL) { + return this.nodeInfo.mirrorURL; + } return `https://nodejs.org/dist`; } getManifest() { @@ -100626,6 +100722,33 @@ class OfficialBuilds extends base_distribution_1.default { isLatestSyntax(versionSpec) { return ['current', 'latest', 'node'].includes(versionSpec); } + downloadFromMirrorURL() { + return __awaiter(this, void 0, void 0, function* () { + const nodeJsVersions = yield this.getMirrorUrlVersions(); + const versions = this.filterVersions(nodeJsVersions); + const evaluatedVersion = this.evaluateVersions(versions); + if (!evaluatedVersion) { + throw new Error(`Unable to find Node version '${this.nodeInfo.versionSpec}' for platform ${this.osPlat} and architecture ${this.nodeInfo.arch} from the provided mirror-url ${this.nodeInfo.mirrorURL}. Please check the mirror-url`); + } + const toolName = this.getNodejsMirrorURLInfo(evaluatedVersion); + try { + const toolPath = yield this.downloadNodejs(toolName); + return toolPath; + } + catch (error) { + if (error instanceof tc.HTTPError && error.httpStatusCode === 404) { + core.error(`Node version ${this.nodeInfo.versionSpec} for platform ${this.osPlat} and architecture ${this.nodeInfo.arch} was found but failed to download. ` + + 'This usually happens when downloadable binaries are not fully updated at https://nodejs.org/. ' + + 'To resolve this issue you may either fall back to the older version or try again later.'); + } + else { + // For any other error type, you can log the error message. + core.error(`An unexpected error occurred like url might not correct`); + } + throw error; + } + }); + } } exports["default"] = OfficialBuilds; @@ -100643,11 +100766,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", ({ value: true })); const base_distribution_1 = __importDefault(__nccwpck_require__(7)); class RcBuild extends base_distribution_1.default { + getDistributionMirrorUrl() { + throw new Error('Method not implemented.'); + } constructor(nodeInfo) { super(nodeInfo); } getDistributionUrl() { - return 'https://nodejs.org/download/rc'; + if (this.nodeInfo.mirrorURL) { + if (this.nodeInfo.mirrorURL != '') { + return this.nodeInfo.mirrorURL; + } + else { + if (this.nodeInfo.mirrorURL === '') { + throw new Error('Mirror URL is empty. Please provide a valid mirror URL.'); + } + else { + throw new Error('Mirror URL is not a valid'); + } + } + } + else { + return 'https://nodejs.org/download/rc'; + } } } exports["default"] = RcBuild; @@ -100671,7 +100812,22 @@ class CanaryBuild extends base_distribution_prerelease_1.default { this.distribution = 'v8-canary'; } getDistributionUrl() { - return 'https://nodejs.org/download/v8-canary'; + if (this.nodeInfo.mirrorURL) { + if (this.nodeInfo.mirrorURL != '') { + return this.nodeInfo.mirrorURL; + } + else { + if (this.nodeInfo.mirrorURL === '') { + throw new Error('Mirror URL is empty. Please provide a valid mirror URL.'); + } + else { + throw new Error('Mirror URL is not a valid'); + } + } + } + else { + return 'https://nodejs.org/download/v8-canary'; + } } } exports["default"] = CanaryBuild; @@ -100748,6 +100904,7 @@ function run() { if (!arch) { arch = os_1.default.arch(); } + const mirrorURL = core.getInput('mirror-url').trim(); // .trim() to remove any accidental spaces if (version) { const token = core.getInput('token'); const auth = !token ? undefined : `token ${token}`; @@ -100758,7 +100915,8 @@ function run() { checkLatest, auth, stable, - arch + arch, + mirrorURL }; const nodeDistribution = (0, installer_factory_1.getNodejsDistribution)(nodejsInfo); yield nodeDistribution.setupNodeJs(); diff --git a/src/distributions/base-distribution.ts b/src/distributions/base-distribution.ts index 70b4b572..2235d28a 100644 --- a/src/distributions/base-distribution.ts +++ b/src/distributions/base-distribution.ts @@ -104,6 +104,31 @@ export default abstract class BaseDistribution { return response.result || []; } + protected async getMirrorUrlVersions(): Promise { + const initialUrl = this.getDistributionUrl(); + + const dataUrl = `${initialUrl}/index.json`; + try { + const response = await this.httpClient.getJson(dataUrl); + return response.result || []; + } catch (err) { + if ( + err instanceof Error && + err.message.includes('getaddrinfo EAI_AGAIN') + ) { + core.error(`Network error: Failed to resolve the server at ${dataUrl}. + Please check your DNS settings or verify that the URL is correct.`); + } else if (err instanceof hc.HttpClientError && err.statusCode === 404) { + core.error(`404 Error: Unable to find versions at ${dataUrl}. + Please verify that the mirror URL is valid.`); + } else { + core.error(`Failed to fetch Node.js versions from ${dataUrl}. + Please check the URL and try again.}`); + } + throw err; + } + } + protected getNodejsDistInfo(version: string) { const osArch: string = this.translateArchToDistUrl(this.nodeInfo.arch); version = semver.clean(version) || ''; @@ -128,6 +153,33 @@ export default abstract class BaseDistribution { }; } + protected getNodejsMirrorURLInfo(version: string) { + const mirrorURL = this.nodeInfo.mirrorURL; + + const osArch: string = this.translateArchToDistUrl(this.nodeInfo.arch); + + version = semver.clean(version) || ''; + const fileName: string = + this.osPlat == 'win32' + ? `node-v${version}-win-${osArch}` + : `node-v${version}-${this.osPlat}-${osArch}`; + const urlFileName: string = + this.osPlat == 'win32' + ? this.nodeInfo.arch === 'arm64' + ? `${fileName}.zip` + : `${fileName}.7z` + : `${fileName}.tar.gz`; + + const url = `${mirrorURL}/v${version}/${urlFileName}`; + + return { + downloadUrl: url, + resolvedVersion: version, + arch: osArch, + fileName: fileName + }; + } + protected async downloadNodejs(info: INodeVersionInfo) { let downloadPath = ''; core.info( @@ -143,9 +195,23 @@ export default abstract class BaseDistribution { ) { return await this.acquireWindowsNodeFromFallbackLocation( info.resolvedVersion, - info.arch + info.arch, + info.downloadUrl ); } + // Handle network-related issues (e.g., DNS resolution failures) + if ( + err instanceof Error && + err.message.includes('getaddrinfo EAI_AGAIN') + ) { + core.error( + `Network error: Failed to resolve the server at ${info.downloadUrl}. + This could be due to a DNS resolution issue. Please verify the URL or check your network connection.` + ); + } + core.error( + `Download failed from ${info.downloadUrl}. Please check the URl and try again.` + ); throw err; } @@ -166,9 +232,11 @@ export default abstract class BaseDistribution { protected async acquireWindowsNodeFromFallbackLocation( version: string, - arch: string = os.arch() + arch: string = os.arch(), + downloadUrl: string ): Promise { const initialUrl = this.getDistributionUrl(); + core.info('url: ' + initialUrl); const osArch: string = this.translateArchToDistUrl(arch); // Create temporary folder to download to @@ -185,6 +253,12 @@ export default abstract class BaseDistribution { core.info(`Downloading only node binary from ${exeUrl}`); + if (downloadUrl != exeUrl) { + core.error( + 'unable to download node binary with the provided URL. Please check and try again' + ); + } + const exePath = await tc.downloadTool(exeUrl); await io.cp(exePath, path.join(tempDir, 'node.exe')); const libPath = await tc.downloadTool(libUrl); diff --git a/src/distributions/base-models.ts b/src/distributions/base-models.ts index 0be93b63..1af61ec8 100644 --- a/src/distributions/base-models.ts +++ b/src/distributions/base-models.ts @@ -4,6 +4,7 @@ export interface NodeInputs { auth?: string; checkLatest: boolean; stable: boolean; + mirrorURL?: string; } export interface INodeVersionInfo { diff --git a/src/distributions/nightly/nightly_builds.ts b/src/distributions/nightly/nightly_builds.ts index 86a89eed..340d4579 100644 --- a/src/distributions/nightly/nightly_builds.ts +++ b/src/distributions/nightly/nightly_builds.ts @@ -1,13 +1,29 @@ import BasePrereleaseNodejs from '../base-distribution-prerelease'; import {NodeInputs} from '../base-models'; +import * as core from '@actions/core'; export default class NightlyNodejs extends BasePrereleaseNodejs { protected distribution = 'nightly'; + constructor(nodeInfo: NodeInputs) { super(nodeInfo); } protected getDistributionUrl(): string { - return 'https://nodejs.org/download/nightly'; + if (this.nodeInfo.mirrorURL) { + if (this.nodeInfo.mirrorURL != '') { + return this.nodeInfo.mirrorURL; + } else { + if (this.nodeInfo.mirrorURL === '') { + throw new Error( + 'Mirror URL is empty. Please provide a valid mirror URL.' + ); + } else { + throw new Error('Mirror URL is not a valid'); + } + } + } else { + return 'https://nodejs.org/download/nightly'; + } } } diff --git a/src/distributions/official_builds/official_builds.ts b/src/distributions/official_builds/official_builds.ts index e56eaf81..d5cd1cb5 100644 --- a/src/distributions/official_builds/official_builds.ts +++ b/src/distributions/official_builds/official_builds.ts @@ -15,115 +15,136 @@ export default class OfficialBuilds extends BaseDistribution { } public async setupNodeJs() { - let manifest: tc.IToolRelease[] | undefined; - let nodeJsVersions: INodeVersion[] | undefined; - const osArch = this.translateArchToDistUrl(this.nodeInfo.arch); - - if (this.isLtsAlias(this.nodeInfo.versionSpec)) { - core.info('Attempt to resolve LTS alias from manifest...'); - - // No try-catch since it's not possible to resolve LTS alias without manifest - manifest = await this.getManifest(); - - this.nodeInfo.versionSpec = this.resolveLtsAliasFromManifest( - this.nodeInfo.versionSpec, - this.nodeInfo.stable, - manifest - ); - } - - if (this.isLatestSyntax(this.nodeInfo.versionSpec)) { - nodeJsVersions = await this.getNodeJsVersions(); - const versions = this.filterVersions(nodeJsVersions); - this.nodeInfo.versionSpec = this.evaluateVersions(versions); - - core.info('getting latest node version...'); - } - - if (this.nodeInfo.checkLatest) { - core.info('Attempt to resolve the latest version from manifest...'); - const resolvedVersion = await this.resolveVersionFromManifest( - this.nodeInfo.versionSpec, - this.nodeInfo.stable, - osArch, - manifest - ); - if (resolvedVersion) { - this.nodeInfo.versionSpec = resolvedVersion; - core.info(`Resolved as '${resolvedVersion}'`); - } else { - core.info( - `Failed to resolve version ${this.nodeInfo.versionSpec} from manifest` + if (this.nodeInfo.mirrorURL) { + if (this.nodeInfo.mirrorURL === '') { + throw new Error( + 'Mirror URL is empty. Please provide a valid mirror URL.' ); } - } - - let toolPath = this.findVersionInHostedToolCacheDirectory(); - - if (toolPath) { - core.info(`Found in cache @ ${toolPath}`); - this.addToolPath(toolPath); - return; - } - - let downloadPath = ''; - try { - core.info(`Attempting to download ${this.nodeInfo.versionSpec}...`); - - const versionInfo = await this.getInfoFromManifest( - this.nodeInfo.versionSpec, - this.nodeInfo.stable, - osArch, - manifest - ); - - if (versionInfo) { - core.info( - `Acquiring ${versionInfo.resolvedVersion} - ${versionInfo.arch} from ${versionInfo.downloadUrl}` - ); - downloadPath = await tc.downloadTool( - versionInfo.downloadUrl, - undefined, - this.nodeInfo.auth - ); - + let downloadPath = ''; + let toolPath = ''; + try { + core.info(`Attempting to download using mirror URL...`); + downloadPath = await this.downloadFromMirrorURL(); // Attempt to download from the mirror + core.info('downloadPath from downloadFromMirrorURL() ' + downloadPath); if (downloadPath) { - toolPath = await this.extractArchive( - downloadPath, - versionInfo, - false + toolPath = downloadPath; + } + } catch (err) { + core.info((err as Error).message); + core.debug((err as Error).stack ?? 'empty stack'); + } + } else { + let manifest: tc.IToolRelease[] | undefined; + let nodeJsVersions: INodeVersion[] | undefined; + const osArch = this.translateArchToDistUrl(this.nodeInfo.arch); + + if (this.isLtsAlias(this.nodeInfo.versionSpec)) { + core.info('Attempt to resolve LTS alias from manifest...'); + + // No try-catch since it's not possible to resolve LTS alias without manifest + manifest = await this.getManifest(); + + this.nodeInfo.versionSpec = this.resolveLtsAliasFromManifest( + this.nodeInfo.versionSpec, + this.nodeInfo.stable, + manifest + ); + } + + if (this.isLatestSyntax(this.nodeInfo.versionSpec)) { + nodeJsVersions = await this.getNodeJsVersions(); + const versions = this.filterVersions(nodeJsVersions); + this.nodeInfo.versionSpec = this.evaluateVersions(versions); + + core.info('getting latest node version...'); + } + + if (this.nodeInfo.checkLatest) { + core.info('Attempt to resolve the latest version from manifest...'); + const resolvedVersion = await this.resolveVersionFromManifest( + this.nodeInfo.versionSpec, + this.nodeInfo.stable, + osArch, + manifest + ); + if (resolvedVersion) { + this.nodeInfo.versionSpec = resolvedVersion; + core.info(`Resolved as '${resolvedVersion}'`); + } else { + core.info( + `Failed to resolve version ${this.nodeInfo.versionSpec} from manifest` ); } - } else { - core.info( - 'Not found in manifest. Falling back to download directly from Node' - ); } - } catch (err) { - // Rate limit? - if ( - err instanceof tc.HTTPError && - (err.httpStatusCode === 403 || err.httpStatusCode === 429) - ) { - core.info( - `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` - ); - } else { - core.info((err as Error).message); + + let toolPath = this.findVersionInHostedToolCacheDirectory(); + + if (toolPath) { + core.info(`Found in cache @ ${toolPath}`); + this.addToolPath(toolPath); + return; } - core.debug((err as Error).stack ?? 'empty stack'); - core.info('Falling back to download directly from Node'); - } - if (!toolPath) { - toolPath = await this.downloadDirectlyFromNode(); - } + let downloadPath = ''; + try { + core.info(`Attempting to download ${this.nodeInfo.versionSpec}...`); - if (this.osPlat != 'win32') { - toolPath = path.join(toolPath, 'bin'); - } + const versionInfo = await this.getInfoFromManifest( + this.nodeInfo.versionSpec, + this.nodeInfo.stable, + osArch, + manifest + ); - core.addPath(toolPath); + if (versionInfo) { + core.info( + `Acquiring ${versionInfo.resolvedVersion} - ${versionInfo.arch} from ${versionInfo.downloadUrl}` + ); + downloadPath = await tc.downloadTool( + versionInfo.downloadUrl, + undefined, + this.nodeInfo.auth + ); + + if (downloadPath) { + toolPath = await this.extractArchive( + downloadPath, + versionInfo, + false + ); + } + } else { + core.info( + 'Not found in manifest. Falling back to download directly from Node' + ); + } + } catch (err) { + // Rate limit? + if ( + err instanceof tc.HTTPError && + (err.httpStatusCode === 403 || err.httpStatusCode === 429) + ) { + core.info( + `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` + ); + } else { + core.info((err as Error).message); + } + core.debug((err as Error).stack ?? 'empty stack'); + core.info('Falling back to download directly from Node'); + } + + if (!toolPath) { + toolPath = await this.downloadDirectlyFromNode(); + } + + if (this.osPlat != 'win32') { + toolPath = path.join(toolPath, 'bin'); + } + + core.addPath(toolPath); + } } protected addToolPath(toolPath: string) { @@ -177,6 +198,9 @@ export default class OfficialBuilds extends BaseDistribution { } protected getDistributionUrl(): string { + if (this.nodeInfo.mirrorURL) { + return this.nodeInfo.mirrorURL; + } return `https://nodejs.org/dist`; } @@ -291,4 +315,38 @@ export default class OfficialBuilds extends BaseDistribution { private isLatestSyntax(versionSpec): boolean { return ['current', 'latest', 'node'].includes(versionSpec); } + + protected async downloadFromMirrorURL() { + const nodeJsVersions = await this.getMirrorUrlVersions(); + const versions = this.filterVersions(nodeJsVersions); + + const evaluatedVersion = this.evaluateVersions(versions); + + if (!evaluatedVersion) { + throw new Error( + `Unable to find Node version '${this.nodeInfo.versionSpec}' for platform ${this.osPlat} and architecture ${this.nodeInfo.arch} from the provided mirror-url ${this.nodeInfo.mirrorURL}. Please check the mirror-url` + ); + } + + const toolName = this.getNodejsMirrorURLInfo(evaluatedVersion); + + try { + const toolPath = await this.downloadNodejs(toolName); + + return toolPath; + } catch (error) { + if (error instanceof tc.HTTPError && error.httpStatusCode === 404) { + core.error( + `Node version ${this.nodeInfo.versionSpec} for platform ${this.osPlat} and architecture ${this.nodeInfo.arch} was found but failed to download. ` + + 'This usually happens when downloadable binaries are not fully updated at https://nodejs.org/. ' + + 'To resolve this issue you may either fall back to the older version or try again later.' + ); + } else { + // For any other error type, you can log the error message. + core.error(`An unexpected error occurred like url might not correct`); + } + + throw error; + } + } } diff --git a/src/distributions/rc/rc_builds.ts b/src/distributions/rc/rc_builds.ts index 40cdb192..c194d4b1 100644 --- a/src/distributions/rc/rc_builds.ts +++ b/src/distributions/rc/rc_builds.ts @@ -1,12 +1,30 @@ import BaseDistribution from '../base-distribution'; import {NodeInputs} from '../base-models'; +import * as core from '@actions/core'; export default class RcBuild extends BaseDistribution { + getDistributionMirrorUrl() { + throw new Error('Method not implemented.'); + } + constructor(nodeInfo: NodeInputs) { super(nodeInfo); } - - getDistributionUrl(): string { - return 'https://nodejs.org/download/rc'; + protected getDistributionUrl(): string { + if (this.nodeInfo.mirrorURL) { + if (this.nodeInfo.mirrorURL != '') { + return this.nodeInfo.mirrorURL; + } else { + if (this.nodeInfo.mirrorURL === '') { + throw new Error( + 'Mirror URL is empty. Please provide a valid mirror URL.' + ); + } else { + throw new Error('Mirror URL is not a valid'); + } + } + } else { + return 'https://nodejs.org/download/rc'; + } } } diff --git a/src/distributions/v8-canary/canary_builds.ts b/src/distributions/v8-canary/canary_builds.ts index 257151b4..4b597dd5 100644 --- a/src/distributions/v8-canary/canary_builds.ts +++ b/src/distributions/v8-canary/canary_builds.ts @@ -1,6 +1,5 @@ import BasePrereleaseNodejs from '../base-distribution-prerelease'; import {NodeInputs} from '../base-models'; - export default class CanaryBuild extends BasePrereleaseNodejs { protected distribution = 'v8-canary'; constructor(nodeInfo: NodeInputs) { @@ -8,6 +7,20 @@ export default class CanaryBuild extends BasePrereleaseNodejs { } protected getDistributionUrl(): string { - return 'https://nodejs.org/download/v8-canary'; + if (this.nodeInfo.mirrorURL) { + if (this.nodeInfo.mirrorURL != '') { + return this.nodeInfo.mirrorURL; + } else { + if (this.nodeInfo.mirrorURL === '') { + throw new Error( + 'Mirror URL is empty. Please provide a valid mirror URL.' + ); + } else { + throw new Error('Mirror URL is not a valid'); + } + } + } else { + return 'https://nodejs.org/download/v8-canary'; + } } } diff --git a/src/main.ts b/src/main.ts index c55c3b00..e142324e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,6 +33,8 @@ export async function run() { arch = os.arch(); } + const mirrorURL = core.getInput('mirror-url').trim(); // .trim() to remove any accidental spaces + if (version) { const token = core.getInput('token'); const auth = !token ? undefined : `token ${token}`; @@ -45,7 +47,8 @@ export async function run() { checkLatest, auth, stable, - arch + arch, + mirrorURL }; const nodeDistribution = getNodejsDistribution(nodejsInfo); await nodeDistribution.setupNodeJs();