diff --git a/README.md b/README.md index 0c554898..4508478c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # setup-node [![basic-validation](https://github.com/actions/setup-node/actions/workflows/basic-validation.yml/badge.svg)](https://github.com/actions/setup-node/actions/workflows/basic-validation.yml) @@ -76,6 +77,10 @@ See [action.yml](action.yml) # Set always-auth option in npmrc file. # Default: '' always-auth: '' + + # Optional mirror to download nodejs binaries from. + # Default: '' + mirror-url: '' ``` @@ -86,7 +91,7 @@ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm ci - run: npm test ``` @@ -103,7 +108,7 @@ The `node-version` input supports the Semantic Versioning Specification, for mor Examples: - - Major versions: `18`, `20` + - Major versions: `20`, `22` - More specific versions: `10.15`, `16.15.1` , `18.4.0` - NVM LTS syntax: `lts/erbium`, `lts/fermium`, `lts/*`, `lts/-n` - Latest release: `*` or `latest`/`current`/`node` @@ -163,7 +168,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [ 14, 16, 18 ] + node: [ 18, 20, 22 ] name: Node ${{ matrix.node }} sample steps: - uses: actions/checkout@v4 @@ -203,6 +208,7 @@ If the runner is not able to access github.com, any Nodejs versions requested du - [Publishing to npmjs and GPR with npm](docs/advanced-usage.md#publish-to-npmjs-and-gpr-with-npm) - [Publishing to npmjs and GPR with yarn](docs/advanced-usage.md#publish-to-npmjs-and-gpr-with-yarn) - [Using private packages](docs/advanced-usage.md#use-private-packages) + - [Using mirror-url)](https://github.com/aparnajyothi-y/setup-node/edit/add-mirror-url/docs/advanced-usage.md#node-mirrors-nodejs-version-mirrors) ## Recommended permissions @@ -224,3 +230,5 @@ Contributions are welcome! See [Contributor's Guide](docs/contributors.md) ## Code of Conduct :wave: Be nice. See [our code of conduct](CODE_OF_CONDUCT.md) + + diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 501741a6..5a3b0f9b 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -13,6 +13,7 @@ import each from 'jest-each'; import * as main from '../src/main'; import * as util from '../src/util'; import OfficialBuilds from '../src/distributions/official_builds/official_builds'; +import {validateMirrorURL} from '../src/util'; describe('main tests', () => { let inputs = {} as any; @@ -280,4 +281,36 @@ describe('main tests', () => { ); }); }); + describe('mirror-url parameter', () => { + beforeEach(() => { + inputs['mirror-url'] = 'https://custom-mirror-url.com'; + }); + + afterEach(() => { + delete inputs['mirror-url']; + }); + + it('Read mirror-url if mirror-url is provided', async () => { + // Arrange + inputs['mirror-url'] = 'https://custom-mirror-url.com'; + + // Act + await main.run(); + + // Assert + expect(inputs['mirror-url']).toBeDefined(); + }); + + it('should throw an error if mirror-url is empty', async () => { + // Arrange + inputs['mirror-url'] = ' '; + + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); // Mock the log function + + // Act & Assert + expect(() => validateMirrorURL(inputs['mirror-url'])).toThrow( + 'Mirror URL is empty. Please provide a valid mirror URL.' + ); + }); + }); }); diff --git a/__tests__/official-installer.test.ts b/__tests__/official-installer.test.ts index 2d8f17cf..274166ab 100644 --- a/__tests__/official-installer.test.ts +++ b/__tests__/official-installer.test.ts @@ -10,6 +10,8 @@ import osm from 'os'; import path from 'path'; import * as main from '../src/main'; import * as auth from '../src/authutil'; +import isLtsAlias from '../src/distributions/official_builds/official_builds'; + import OfficialBuilds from '../src/distributions/official_builds/official_builds'; import {INodeVersion} from '../src/distributions/base-models'; @@ -828,4 +830,35 @@ describe('setup-node', () => { } ); }); + describe('mirror-url parameter', () => { + it('default if mirror url is not provided', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + inputs['node-version'] = '11'; + inputs['check-latest'] = 'true'; + inputs['always-auth'] = false; + inputs['token'] = 'faketoken'; + + dlSpy.mockImplementation(async () => '/some/temp/path'); + const toolPath = path.normalize('/cache/node/12.11.0/x64'); + exSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + const dlmirrorSpy = jest.fn(); + dlmirrorSpy.mockImplementation(async () => 'mocked-download-path'); + await main.run(); + + const expPath = path.join(toolPath, 'bin'); + + expect(dlSpy).toHaveBeenCalled(); + expect(exSpy).toHaveBeenCalled(); + + expect(logSpy).toHaveBeenCalledWith( + 'Attempt to resolve the latest version from manifest...' + ); + + expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`); + }); + }); }); diff --git a/action.yml b/action.yml index 99db5869..35302813 100644 --- a/action.yml +++ b/action.yml @@ -16,6 +16,9 @@ inputs: default: false registry-url: description: 'Optional registry to set up for auth. Will set the registry in a project level .npmrc and .yarnrc file, and set up auth to read in from env.NODE_AUTH_TOKEN.' + mirror-url: + description: 'Custom mirror URL to download Node.js' + required: false scope: description: 'Optional scope for authenticating against scoped registries. Will fall back to the repository owner when using the GitHub Packages registry (https://npm.pkg.github.com/).' token: diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js index a069cabd..2b4610e6 100644 --- a/dist/cache-save/index.js +++ b/dist/cache-save/index.js @@ -88239,7 +88239,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.unique = exports.printEnvDetailsAndSetOutput = exports.getNodeVersionFromFile = void 0; +exports.unique = exports.validateMirrorURL = exports.printEnvDetailsAndSetOutput = exports.getNodeVersionFromFile = void 0; const core = __importStar(__nccwpck_require__(2186)); const exec = __importStar(__nccwpck_require__(1514)); const io = __importStar(__nccwpck_require__(7436)); @@ -88327,6 +88327,15 @@ function getToolVersion(tool, options) { } }); } +function validateMirrorURL(mirrorURL) { + if (mirrorURL === ' ' || mirrorURL.trim() === 'undefined') { + throw new Error('Mirror URL is empty. Please provide a valid mirror URL.'); + } + else { + return mirrorURL; + } +} +exports.validateMirrorURL = validateMirrorURL; const unique = () => { const encountered = new Set(); return (value) => { diff --git a/dist/setup/index.js b/dist/setup/index.js index 63eef878..8dfef4bd 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -97295,6 +97295,29 @@ 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.setFailed(`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.setFailed(`404 Error: Unable to find versions at ${dataUrl}.Please verify that the mirror URL is valid.`); + } + else { + core.setFailed(`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) || ''; @@ -97315,6 +97338,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 = ''; @@ -97326,8 +97369,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); @@ -97343,8 +97393,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)()}`; @@ -97358,6 +97409,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); @@ -97533,7 +97587,12 @@ class NightlyNodejs extends base_distribution_prerelease_1.default { this.distribution = 'nightly'; } getDistributionUrl() { - return 'https://nodejs.org/download/nightly'; + if (this.nodeInfo.mirrorURL) { + return this.nodeInfo.mirrorURL; + } + else { + return 'https://nodejs.org/download/nightly'; + } } } exports["default"] = NightlyNodejs; @@ -97593,72 +97652,84 @@ 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}'`); + if (this.nodeInfo.mirrorURL) { + try { + core.info(`Attempting to download using mirror URL...`); + yield this.downloadFromMirrorURL(); // Attempt to download from the mirror } - else { - core.info(`Failed to resolve version ${this.nodeInfo.versionSpec} from manifest`); + catch (err) { + core.setFailed(err.message); } } - 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('Setup Node.js'); + 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`); } } - else { - core.info('Not found in manifest. Falling back to download directly from Node'); + let toolPath = this.findVersionInHostedToolCacheDirectory(); + if (toolPath) { + core.info(`Found in cache @ ${toolPath}`); + this.addToolPath(toolPath); + return; } - } - 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`); + 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'); + } } - else { - core.info(err.message); + 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((_a = err.stack) !== null && _a !== void 0 ? _a : 'empty stack'); + core.info('Falling back to download directly from Node'); } - 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(); + } + if (this.osPlat != 'win32') { + toolPath = path_1.default.join(toolPath, 'bin'); + } + core.addPath(toolPath); } - if (!toolPath) { - toolPath = yield this.downloadDirectlyFromNode(); - } - if (this.osPlat != 'win32') { - toolPath = path_1.default.join(toolPath, 'bin'); - } - core.addPath(toolPath); }); } addToolPath(toolPath) { @@ -97700,6 +97771,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() { @@ -97767,6 +97841,32 @@ 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.setFailed(`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 in the provided mirror-url' + + 'To resolve this issue you may either fall back to the older version or try again later.'); + } + else { + core.setFailed(`An unexpected error occurred like url might not correct`); + } + throw error; + } + }); + } } exports["default"] = OfficialBuilds; @@ -97788,7 +97888,12 @@ class RcBuild extends base_distribution_1.default { super(nodeInfo); } getDistributionUrl() { - return 'https://nodejs.org/download/rc'; + if (this.nodeInfo.mirrorURL) { + return this.nodeInfo.mirrorURL; + } + else { + return 'https://nodejs.org/download/rc'; + } } } exports["default"] = RcBuild; @@ -97812,7 +97917,12 @@ class CanaryBuild extends base_distribution_prerelease_1.default { this.distribution = 'v8-canary'; } getDistributionUrl() { - return 'https://nodejs.org/download/v8-canary'; + if (this.nodeInfo.mirrorURL) { + return this.nodeInfo.mirrorURL; + } + else { + return 'https://nodejs.org/download/v8-canary'; + } } } exports["default"] = CanaryBuild; @@ -97889,6 +97999,7 @@ function run() { if (!arch) { arch = os_1.default.arch(); } + const mirrorURL = core.getInput('mirror-url'); if (version) { const token = core.getInput('token'); const auth = !token ? undefined : `token ${token}`; @@ -97899,7 +98010,8 @@ function run() { checkLatest, auth, stable, - arch + arch, + mirrorURL }; const nodeDistribution = (0, installer_factory_1.getNodejsDistribution)(nodejsInfo); yield nodeDistribution.setupNodeJs(); @@ -97993,7 +98105,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.unique = exports.printEnvDetailsAndSetOutput = exports.getNodeVersionFromFile = void 0; +exports.unique = exports.validateMirrorURL = exports.printEnvDetailsAndSetOutput = exports.getNodeVersionFromFile = void 0; const core = __importStar(__nccwpck_require__(2186)); const exec = __importStar(__nccwpck_require__(1514)); const io = __importStar(__nccwpck_require__(7436)); @@ -98081,6 +98193,15 @@ function getToolVersion(tool, options) { } }); } +function validateMirrorURL(mirrorURL) { + if (mirrorURL === ' ' || mirrorURL.trim() === 'undefined') { + throw new Error('Mirror URL is empty. Please provide a valid mirror URL.'); + } + else { + return mirrorURL; + } +} +exports.validateMirrorURL = validateMirrorURL; const unique = () => { const encountered = new Set(); return (value) => { diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index bf62e071..ac61e64d 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -1,3 +1,5 @@ + + ## Working with lockfiles All supported package managers recommend that you **always** commit the lockfile, although implementations vary doing so generally provides the following benefits: @@ -76,10 +78,10 @@ When using the `package.json` input, the action will look for `volta.node` first ```json { "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "volta": { - "node": "16.0.0" + "node": "22.0.0" } } ``` @@ -100,7 +102,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '14' + node-version: '22' architecture: 'x64' # optional, x64 or x86. If not specified, x64 will be used by default - run: npm ci - run: npm test @@ -239,7 +241,7 @@ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '14' + node-version: '22' cache: 'yarn' - run: yarn install --frozen-lockfile # optional, --immutable - run: yarn test @@ -261,7 +263,7 @@ steps: version: 6.32.9 - uses: actions/setup-node@v4 with: - node-version: '14' + node-version: '22' cache: 'pnpm' - run: pnpm install - run: pnpm test @@ -277,7 +279,7 @@ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '14' + node-version: '22' cache: 'npm' cache-dependency-path: '**/package-lock.json' - run: npm ci @@ -290,7 +292,7 @@ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '14' + node-version: '22' cache: 'npm' cache-dependency-path: | server/app/package-lock.json @@ -312,9 +314,9 @@ jobs: - macos-latest - windows-latest node_version: - - 12 - - 14 - - 16 + - 18 + - 20 + - 22 architecture: - x64 # an extra windows-x86 run: @@ -340,7 +342,7 @@ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '14.x' + node-version: '22' registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm publish @@ -360,7 +362,7 @@ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '14.x' + node-version: '22' registry-url: - run: yarn install --frozen-lockfile - run: yarn publish @@ -380,7 +382,7 @@ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '14.x' + node-version: '22' registry-url: 'https://registry.npmjs.org' # Skip post-install scripts here, as a malicious # script could steal NODE_AUTH_TOKEN. @@ -418,3 +420,15 @@ 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. + +### Using mirror-url +You can use the new mirror-url parameter to specify a custom mirror for downloading node.js versions from a different location + +```yaml +steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + mirror-url: 'https://mirror.example.com/node' +``` \ No newline at end of file diff --git a/src/distributions/base-distribution.ts b/src/distributions/base-distribution.ts index 70b4b572..21923ab4 100644 --- a/src/distributions/base-distribution.ts +++ b/src/distributions/base-distribution.ts @@ -104,6 +104,34 @@ 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.setFailed( + `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.setFailed( + `404 Error: Unable to find versions at ${dataUrl}.Please verify that the mirror URL is valid.` + ); + } else { + core.setFailed( + `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 +156,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 +198,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 +235,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 +256,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..ae0e6265 100644 --- a/src/distributions/nightly/nightly_builds.ts +++ b/src/distributions/nightly/nightly_builds.ts @@ -3,11 +3,16 @@ import {NodeInputs} from '../base-models'; 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) { + return this.nodeInfo.mirrorURL; + } 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..fe35bae4 100644 --- a/src/distributions/official_builds/official_builds.ts +++ b/src/distributions/official_builds/official_builds.ts @@ -15,115 +15,125 @@ 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.nodeInfo.mirrorURL) { + try { + core.info(`Attempting to download using mirror URL...`); + await this.downloadFromMirrorURL(); // Attempt to download from the mirror + } catch (err) { + core.setFailed((err as Error).message); + } + } else { + core.info('Setup Node.js'); + 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...'); + 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(); + // 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` + this.nodeInfo.versionSpec = this.resolveLtsAliasFromManifest( + this.nodeInfo.versionSpec, + this.nodeInfo.stable, + manifest ); } - } - let toolPath = this.findVersionInHostedToolCacheDirectory(); + if (this.isLatestSyntax(this.nodeInfo.versionSpec)) { + nodeJsVersions = await this.getNodeJsVersions(); + const versions = this.filterVersions(nodeJsVersions); + this.nodeInfo.versionSpec = this.evaluateVersions(versions); - if (toolPath) { - core.info(`Found in cache @ ${toolPath}`); - this.addToolPath(toolPath); - return; - } + core.info('getting latest node version...'); + } - 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}` + 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 ); - downloadPath = await tc.downloadTool( - versionInfo.downloadUrl, - undefined, - this.nodeInfo.auth - ); - - if (downloadPath) { - toolPath = await this.extractArchive( - downloadPath, - versionInfo, - false + 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 +187,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 +304,39 @@ 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.setFailed( + `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 in the provided mirror-url' + + 'To resolve this issue you may either fall back to the older version or try again later.' + ); + } else { + core.setFailed( + `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..945c270d 100644 --- a/src/distributions/rc/rc_builds.ts +++ b/src/distributions/rc/rc_builds.ts @@ -5,8 +5,11 @@ export default class RcBuild extends BaseDistribution { constructor(nodeInfo: NodeInputs) { super(nodeInfo); } - - getDistributionUrl(): string { - return 'https://nodejs.org/download/rc'; + protected getDistributionUrl(): string { + if (this.nodeInfo.mirrorURL) { + return this.nodeInfo.mirrorURL; + } 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..53671ace 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,10 @@ export default class CanaryBuild extends BasePrereleaseNodejs { } protected getDistributionUrl(): string { - return 'https://nodejs.org/download/v8-canary'; + if (this.nodeInfo.mirrorURL) { + return this.nodeInfo.mirrorURL; + } else { + return 'https://nodejs.org/download/v8-canary'; + } } } diff --git a/src/main.ts b/src/main.ts index c55c3b00..a49427a0 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'); + 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(); diff --git a/src/util.ts b/src/util.ts index bbe25ddf..232c5db7 100644 --- a/src/util.ts +++ b/src/util.ts @@ -97,7 +97,13 @@ async function getToolVersion(tool: string, options: string[]) { return ''; } } - +export function validateMirrorURL(mirrorURL) { + if (mirrorURL === ' ' || mirrorURL.trim() === 'undefined') { + throw new Error('Mirror URL is empty. Please provide a valid mirror URL.'); + } else { + return mirrorURL; + } +} export const unique = () => { const encountered = new Set(); return (value: unknown): boolean => {