From bcde537b6307f81252c5987f38162484e3c2a6b1 Mon Sep 17 00:00:00 2001 From: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:16:50 +0530 Subject: [PATCH] update with enhance code and test --- .../distributors/graalvm-installer.test.ts | 242 ++++++++++++++++-- dist/setup/index.js | 110 +++++--- src/distributions/graalvm/installer.ts | 185 ++++++++++--- 3 files changed, 441 insertions(+), 96 deletions(-) diff --git a/__tests__/distributors/graalvm-installer.test.ts b/__tests__/distributors/graalvm-installer.test.ts index 1fa671c4..d2dda9f6 100644 --- a/__tests__/distributors/graalvm-installer.test.ts +++ b/__tests__/distributors/graalvm-installer.test.ts @@ -21,7 +21,8 @@ jest.mock('../../src/util', () => ({ jest.mock('fs', () => ({ ...jest.requireActual('fs'), - readdirSync: jest.fn() + readdirSync: jest.fn(), + existsSync: jest.fn() })); beforeAll(() => { @@ -106,11 +107,13 @@ describe('GraalVMDistribution', () => { (util.extractJdkFile as jest.Mock).mockResolvedValue('/tmp/extracted'); // Mock renameWinArchive - it returns the same path (no renaming) - // But it appears the implementation might not even call this (util.renameWinArchive as jest.Mock).mockImplementation((p: string) => p); (util.getDownloadArchiveExtension as jest.Mock).mockReturnValue('tar.gz'); + // Mock fs.existsSync to return true for extracted path + (fs.existsSync as jest.Mock).mockReturnValue(true); + (fs.readdirSync as jest.Mock).mockReturnValue(['graalvm-jdk-17.0.5']); jest @@ -130,6 +133,9 @@ describe('GraalVMDistribution', () => { 'tar.gz' ); + // Verify path existence check + expect(fs.existsSync).toHaveBeenCalledWith('/tmp/extracted'); + // Verify directory reading expect(fs.readdirSync).toHaveBeenCalledWith('/tmp/extracted'); @@ -154,16 +160,59 @@ describe('GraalVMDistribution', () => { expect(core.info).toHaveBeenCalledWith('Extracting Java archive...'); }); - it('should verify that renameWinArchive is available but may not be called', () => { - // Just verify the mock is set up correctly - const originalPath = '/tmp/archive.tar.gz'; + it('should throw error when extracted path does not exist', async () => { + (fs.existsSync as jest.Mock).mockReturnValue(false); - // Call the mock directly to verify it works - const result = util.renameWinArchive(originalPath); + await expect( + (distribution as any).downloadTool(javaRelease) + ).rejects.toThrow( + 'Extraction failed: path /tmp/extracted does not exist' + ); - // Verify it returns the same path (no renaming) - expect(result).toBe(originalPath); - expect(util.renameWinArchive).toHaveBeenCalledWith(originalPath); + expect(core.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to download and extract GraalVM:') + ); + }); + + it('should throw error when extracted directory is empty', async () => { + (fs.existsSync as jest.Mock).mockReturnValue(true); + (fs.readdirSync as jest.Mock).mockReturnValue([]); + + await expect( + (distribution as any).downloadTool(javaRelease) + ).rejects.toThrow( + 'Extraction failed: no files found in extracted directory' + ); + + expect(core.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to download and extract GraalVM:') + ); + }); + + it('should handle download errors', async () => { + const downloadError = new Error('Network error during download'); + (tc.downloadTool as jest.Mock).mockRejectedValue(downloadError); + + await expect( + (distribution as any).downloadTool(javaRelease) + ).rejects.toThrow('Network error during download'); + + expect(core.error).toHaveBeenCalledWith( + 'Failed to download and extract GraalVM: Error: Network error during download' + ); + }); + + it('should handle extraction errors', async () => { + const extractError = new Error('Failed to extract archive'); + (util.extractJdkFile as jest.Mock).mockRejectedValue(extractError); + + await expect( + (distribution as any).downloadTool(javaRelease) + ).rejects.toThrow('Failed to extract archive'); + + expect(core.error).toHaveBeenCalledWith( + 'Failed to download and extract GraalVM: Error: Failed to extract archive' + ); }); it('should handle different archive extensions', async () => { @@ -195,6 +244,38 @@ describe('GraalVMDistribution', () => { jest.spyOn(distribution, 'getPlatform').mockReturnValue('linux'); }); + describe('input validation', () => { + it('should throw error for null version range', async () => { + await expect( + (distribution as any).findPackageForDownload(null) + ).rejects.toThrow('Version range is required and must be a string'); + }); + + it('should throw error for undefined version range', async () => { + await expect( + (distribution as any).findPackageForDownload(undefined) + ).rejects.toThrow('Version range is required and must be a string'); + }); + + it('should throw error for empty string version range', async () => { + await expect( + (distribution as any).findPackageForDownload('') + ).rejects.toThrow('Version range is required and must be a string'); + }); + + it('should throw error for non-string version range', async () => { + await expect( + (distribution as any).findPackageForDownload(123) + ).rejects.toThrow('Version range is required and must be a string'); + }); + + it('should throw error for invalid version format', async () => { + await expect( + (distribution as any).findPackageForDownload('abc') + ).rejects.toThrow('Invalid version format: abc'); + }); + }); + describe('stable builds', () => { it('should construct correct URL for specific version', async () => { const mockResponse = { @@ -236,13 +317,17 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findPackageForDownload('17') - ).rejects.toThrow('Unsupported architecture: x86'); + ).rejects.toThrow( + 'Unsupported architecture: x86. Supported architectures are: x64, aarch64' + ); }); it('should throw error for JDK versions less than 17', async () => { await expect( (distribution as any).findPackageForDownload('11') - ).rejects.toThrow('GraalVM is only supported for JDK 17 and later'); + ).rejects.toThrow( + 'GraalVM is only supported for JDK 17 and later. Requested version: 11' + ); }); it('should throw error for non-jdk package types', async () => { @@ -265,10 +350,54 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findPackageForDownload('17.0.99') - ).rejects.toThrow('Could not find GraalVM for SemVer 17.0.99'); + ).rejects.toThrow( + 'Could not find GraalVM for SemVer 17.0.99. Please check if this version is available at https://download.oracle.com/graalvm' + ); }); - it('should throw error for other HTTP errors', async () => { + it('should throw error for unauthorized access (401)', async () => { + const mockResponse = { + message: {statusCode: 401} + } as http.HttpClientResponse; + mockHttpClient.head.mockResolvedValue(mockResponse); + + await expect( + (distribution as any).findPackageForDownload('17') + ).rejects.toThrow( + 'Access denied when downloading GraalVM. Status code: 401. Please check your credentials or permissions.' + ); + }); + + it('should throw error for forbidden access (403)', async () => { + const mockResponse = { + message: {statusCode: 403} + } as http.HttpClientResponse; + mockHttpClient.head.mockResolvedValue(mockResponse); + + await expect( + (distribution as any).findPackageForDownload('17') + ).rejects.toThrow( + 'Access denied when downloading GraalVM. Status code: 403. Please check your credentials or permissions.' + ); + }); + + it('should throw error for other HTTP errors with status message', async () => { + const mockResponse = { + message: { + statusCode: 500, + statusMessage: 'Internal Server Error' + } + } as http.HttpClientResponse; + mockHttpClient.head.mockResolvedValue(mockResponse); + + await expect( + (distribution as any).findPackageForDownload('17') + ).rejects.toThrow( + 'HTTP request for GraalVM failed with status code: 500 (Internal Server Error)' + ); + }); + + it('should throw error for other HTTP errors without status message', async () => { const mockResponse = { message: {statusCode: 500} } as http.HttpClientResponse; @@ -277,7 +406,7 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findPackageForDownload('17') ).rejects.toThrow( - 'Http request for GraalVM failed with status code: 500' + 'HTTP request for GraalVM failed with status code: 500 (Unknown error)' ); }); }); @@ -368,6 +497,11 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findPackageForDownload('23') ).rejects.toThrow("Unable to find latest version for '23-ea'"); + + // Verify error logging + expect(core.error).toHaveBeenCalledWith( + 'Available versions: 23-ea-20240716' + ); }); it('should throw error when no matching file for architecture in EA build', async () => { @@ -401,7 +535,14 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findPackageForDownload('23') - ).rejects.toThrow("Unable to find file metadata for '23-ea'"); + ).rejects.toThrow( + `Unable to find file for architecture 'x64' and platform '${currentPlatform}'` + ); + + // Verify error logging + expect(core.error).toHaveBeenCalledWith( + expect.stringContaining('Available files for architecture x64:') + ); }); it('should throw error when no matching platform in EA build', async () => { @@ -430,9 +571,14 @@ describe('GraalVMDistribution', () => { .spyOn(distribution as any, 'distributionArchitecture') .mockReturnValue('x64'); + const currentPlatform = + process.platform === 'win32' ? 'windows' : process.platform; + await expect( (distribution as any).findPackageForDownload('23') - ).rejects.toThrow("Unable to find file metadata for '23-ea'"); + ).rejects.toThrow( + `Unable to find file for architecture 'x64' and platform '${currentPlatform}'` + ); }); it('should throw error when filename does not start with graalvm-jdk-', async () => { @@ -466,7 +612,9 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findPackageForDownload('23') - ).rejects.toThrow("Unable to find file metadata for '23-ea'"); + ).rejects.toThrow( + "Invalid filename format: wrong-prefix-23_linux-x64_bin.tar.gz. Expected to start with 'graalvm-jdk-'" + ); }); it('should throw error when EA version JSON is not found', async () => { @@ -478,7 +626,9 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findPackageForDownload('23') - ).rejects.toThrow("No GraalVM EA build found for version '23-ea'"); + ).rejects.toThrow( + "No GraalVM EA build found for version '23-ea'. Please check if the version is correct." + ); }); }); }); @@ -540,6 +690,16 @@ describe('GraalVMDistribution', () => { url: 'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz', version: '23-ea-20240716' }); + + // Verify debug logging + expect(core.debug).toHaveBeenCalledWith('Searching for EA build: 23-ea'); + expect(core.debug).toHaveBeenCalledWith('Found 2 EA versions'); + expect(core.debug).toHaveBeenCalledWith( + 'Latest version found: 23-ea-20240716' + ); + expect(core.debug).toHaveBeenCalledWith( + 'Download URL: https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz' + ); }); it('should throw error when no latest version found', async () => { @@ -549,6 +709,10 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findEABuildDownloadUrl('23-ea') ).rejects.toThrow("Unable to find latest version for '23-ea'"); + + expect(core.error).toHaveBeenCalledWith( + 'Available versions: 23-ea-20240716, 23-ea-20240709' + ); }); it('should throw error when no matching file for architecture', async () => { @@ -570,7 +734,13 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findEABuildDownloadUrl('23-ea') - ).rejects.toThrow("Unable to find file metadata for '23-ea'"); + ).rejects.toThrow( + `Unable to find file for architecture 'x64' and platform '${currentPlatform}'` + ); + + expect(core.error).toHaveBeenCalledWith( + expect.stringContaining('Available files for architecture x64:') + ); }); it('should throw error when filename does not start with graalvm-jdk-', async () => { @@ -592,7 +762,9 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findEABuildDownloadUrl('23-ea') - ).rejects.toThrow("Unable to find file metadata for '23-ea'"); + ).rejects.toThrow( + "Invalid filename format: wrong-name.tar.gz. Expected to start with 'graalvm-jdk-'" + ); }); it('should work with aarch64 architecture', async () => { @@ -631,7 +803,9 @@ describe('GraalVMDistribution', () => { await expect( (distribution as any).findEABuildDownloadUrl('23-ea') - ).rejects.toThrow("Unable to find file metadata for '23-ea'"); + ).rejects.toThrow( + `Unable to find file for architecture 'x64' and platform '${currentPlatform}'` + ); }); }); @@ -666,11 +840,29 @@ describe('GraalVMDistribution', () => { ); }); - it('should handle HTTP errors properly', async () => { - mockHttpClient.getJson.mockRejectedValue(new Error('Network error')); + it('should handle 404 errors with specific message', async () => { + const error404 = new Error('Not Found: 404'); + mockHttpClient.getJson.mockRejectedValue(error404); await expect((distribution as any).fetchEAJson('23-ea')).rejects.toThrow( - 'Network error' + "GraalVM EA version '23-ea' not found. Please verify the version exists in the EA builds repository." + ); + }); + + it('should handle generic HTTP errors with context', async () => { + const networkError = new Error('Network timeout'); + mockHttpClient.getJson.mockRejectedValue(networkError); + + await expect((distribution as any).fetchEAJson('23-ea')).rejects.toThrow( + "Failed to fetch GraalVM EA version information for '23-ea': Network timeout" + ); + }); + + it('should handle non-Error exceptions', async () => { + mockHttpClient.getJson.mockRejectedValue('String error'); + + await expect((distribution as any).fetchEAJson('23-ea')).rejects.toThrow( + "Failed to fetch GraalVM EA version information for '23-ea'" ); }); }); diff --git a/dist/setup/index.js b/dist/setup/index.js index e2ed8559..81eac67f 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -130530,31 +130530,51 @@ const util_1 = __nccwpck_require__(92629); const GRAALVM_DL_BASE = 'https://download.oracle.com/graalvm'; const IS_WINDOWS = process.platform === 'win32'; const GRAALVM_PLATFORM = IS_WINDOWS ? 'windows' : process.platform; +const GRAALVM_MIN_VERSION = 17; +const SUPPORTED_ARCHITECTURES = ['x64', 'aarch64']; class GraalVMDistribution extends base_installer_1.JavaBase { constructor(installerOptions) { super('GraalVM', installerOptions); } downloadTool(javaRelease) { return __awaiter(this, void 0, void 0, function* () { - core.info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = yield tc.downloadTool(javaRelease.url); - core.info(`Extracting Java archive...`); - const extension = (0, util_1.getDownloadArchiveExtension)(); - if (IS_WINDOWS) { - javaArchivePath = (0, util_1.renameWinArchive)(javaArchivePath); + try { + core.info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); + let javaArchivePath = yield tc.downloadTool(javaRelease.url); + core.info(`Extracting Java archive...`); + const extension = (0, util_1.getDownloadArchiveExtension)(); + if (IS_WINDOWS) { + javaArchivePath = (0, util_1.renameWinArchive)(javaArchivePath); + } + const extractedJavaPath = yield (0, util_1.extractJdkFile)(javaArchivePath, extension); + // Add validation for extracted path + if (!fs_1.default.existsSync(extractedJavaPath)) { + throw new Error(`Extraction failed: path ${extractedJavaPath} does not exist`); + } + const dirContents = fs_1.default.readdirSync(extractedJavaPath); + if (dirContents.length === 0) { + throw new Error('Extraction failed: no files found in extracted directory'); + } + const archivePath = path_1.default.join(extractedJavaPath, dirContents[0]); + const version = this.getToolcacheVersionName(javaRelease.version); + const javaPath = yield tc.cacheDir(archivePath, this.toolcacheFolderName, version, this.architecture); + return { version: javaRelease.version, path: javaPath }; + } + catch (error) { + core.error(`Failed to download and extract GraalVM: ${error}`); + throw error; } - const extractedJavaPath = yield (0, util_1.extractJdkFile)(javaArchivePath, extension); - const archivePath = path_1.default.join(extractedJavaPath, fs_1.default.readdirSync(extractedJavaPath)[0]); - const version = this.getToolcacheVersionName(javaRelease.version); - const javaPath = yield tc.cacheDir(archivePath, this.toolcacheFolderName, version, this.architecture); - return { version: javaRelease.version, path: javaPath }; }); } findPackageForDownload(range) { return __awaiter(this, void 0, void 0, function* () { + // Add input validation + if (!range || typeof range !== 'string') { + throw new Error('Version range is required and must be a string'); + } const arch = this.distributionArchitecture(); - if (!['x64', 'aarch64'].includes(arch)) { - throw new Error(`Unsupported architecture: ${this.architecture}`); + if (!SUPPORTED_ARCHITECTURES.includes(arch)) { + throw new Error(`Unsupported architecture: ${this.architecture}. Supported architectures are: ${SUPPORTED_ARCHITECTURES.join(', ')}`); } if (!this.stable) { return this.findEABuildDownloadUrl(`${range}-ea`); @@ -130565,10 +130585,14 @@ class GraalVMDistribution extends base_installer_1.JavaBase { const platform = this.getPlatform(); const extension = (0, util_1.getDownloadArchiveExtension)(); const major = range.includes('.') ? range.split('.')[0] : range; - const fileUrl = this.constructFileUrl(range, major, platform, arch, extension); - if (parseInt(major) < 17) { - throw new Error('GraalVM is only supported for JDK 17 and later'); + const majorVersion = parseInt(major); + if (isNaN(majorVersion)) { + throw new Error(`Invalid version format: ${range}`); } + if (majorVersion < GRAALVM_MIN_VERSION) { + throw new Error(`GraalVM is only supported for JDK ${GRAALVM_MIN_VERSION} and later. Requested version: ${major}`); + } + const fileUrl = this.constructFileUrl(range, major, platform, arch, extension); const response = yield this.http.head(fileUrl); this.handleHttpResponse(response, range); return { url: fileUrl, version: range }; @@ -130580,43 +130604,71 @@ class GraalVMDistribution extends base_installer_1.JavaBase { : `${GRAALVM_DL_BASE}/${range}/latest/graalvm-jdk-${range}_${platform}-${arch}_bin.${extension}`; } handleHttpResponse(response, range) { - if (response.message.statusCode === http_client_1.HttpCodes.NotFound) { - throw new Error(`Could not find GraalVM for SemVer ${range}`); + const statusCode = response.message.statusCode; + if (statusCode === http_client_1.HttpCodes.NotFound) { + throw new Error(`Could not find GraalVM for SemVer ${range}. Please check if this version is available at ${GRAALVM_DL_BASE}`); } - if (response.message.statusCode !== http_client_1.HttpCodes.OK) { - throw new Error(`Http request for GraalVM failed with status code: ${response.message.statusCode}`); + if (statusCode === http_client_1.HttpCodes.Unauthorized || + statusCode === http_client_1.HttpCodes.Forbidden) { + throw new Error(`Access denied when downloading GraalVM. Status code: ${statusCode}. Please check your credentials or permissions.`); + } + if (statusCode !== http_client_1.HttpCodes.OK) { + throw new Error(`HTTP request for GraalVM failed with status code: ${statusCode} (${response.message.statusMessage || 'Unknown error'})`); } } findEABuildDownloadUrl(javaEaVersion) { return __awaiter(this, void 0, void 0, function* () { + core.debug(`Searching for EA build: ${javaEaVersion}`); const versions = yield this.fetchEAJson(javaEaVersion); + core.debug(`Found ${versions.length} EA versions`); const latestVersion = versions.find(v => v.latest); if (!latestVersion) { + core.error(`Available versions: ${versions.map(v => v.version).join(', ')}`); throw new Error(`Unable to find latest version for '${javaEaVersion}'`); } + core.debug(`Latest version found: ${latestVersion.version}`); const arch = this.distributionArchitecture(); const file = latestVersion.files.find(f => f.arch === arch && f.platform === GRAALVM_PLATFORM); - if (!file || !file.filename.startsWith('graalvm-jdk-')) { - throw new Error(`Unable to find file metadata for '${javaEaVersion}'`); + if (!file) { + core.error(`Available files for architecture ${arch}: ${JSON.stringify(latestVersion.files)}`); + throw new Error(`Unable to find file for architecture '${arch}' and platform '${GRAALVM_PLATFORM}'`); } + if (!file.filename.startsWith('graalvm-jdk-')) { + throw new Error(`Invalid filename format: ${file.filename}. Expected to start with 'graalvm-jdk-'`); + } + const downloadUrl = `${latestVersion.download_base_url}${file.filename}`; + core.debug(`Download URL: ${downloadUrl}`); return { - url: `${latestVersion.download_base_url}${file.filename}`, + url: downloadUrl, version: latestVersion.version }; }); } fetchEAJson(javaEaVersion) { + var _a; return __awaiter(this, void 0, void 0, function* () { const url = `https://api.github.com/repos/graalvm/oracle-graalvm-ea-builds/contents/versions/${javaEaVersion}.json?ref=main`; const headers = (0, util_1.getGitHubHttpHeaders)(); core.debug(`Trying to fetch available version info for GraalVM EA builds from '${url}'`); - const fetchedJson = yield this.http - .getJson(url, headers) - .then(res => res.result); - if (!fetchedJson) { - throw new Error(`No GraalVM EA build found for version '${javaEaVersion}'. Please check if the version is correct.`); + try { + const response = yield this.http.getJson(url, headers); + if (!response.result) { + throw new Error(`No GraalVM EA build found for version '${javaEaVersion}'. Please check if the version is correct.`); + } + return response.result; + } + catch (error) { + if (error instanceof Error) { + // Check if it's a 404 error (file not found) + if ((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('404')) { + throw new Error(`GraalVM EA version '${javaEaVersion}' not found. Please verify the version exists in the EA builds repository.`); + } + // Re-throw with more context + throw new Error(`Failed to fetch GraalVM EA version information for '${javaEaVersion}': ${error.message}`); + } + // If it's not an Error instance, throw a generic error + throw new Error(`Failed to fetch GraalVM EA version information for '${javaEaVersion}'`); } - return fetchedJson; }); } getPlatform(platform = process.platform) { diff --git a/src/distributions/graalvm/installer.ts b/src/distributions/graalvm/installer.ts index ecf3e8a8..6df960c7 100644 --- a/src/distributions/graalvm/installer.ts +++ b/src/distributions/graalvm/installer.ts @@ -20,6 +20,10 @@ import { const GRAALVM_DL_BASE = 'https://download.oracle.com/graalvm'; const IS_WINDOWS = process.platform === 'win32'; const GRAALVM_PLATFORM = IS_WINDOWS ? 'windows' : process.platform; +const GRAALVM_MIN_VERSION = 17; +const SUPPORTED_ARCHITECTURES = ['x64', 'aarch64'] as const; +type SupportedArchitecture = (typeof SUPPORTED_ARCHITECTURES)[number]; +type OsVersions = 'linux' | 'macos' | 'windows'; export class GraalVMDistribution extends JavaBase { constructor(installerOptions: JavaInstallerOptions) { @@ -29,38 +33,67 @@ export class GraalVMDistribution extends JavaBase { protected async downloadTool( javaRelease: JavaDownloadRelease ): Promise { - core.info( - `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` - ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + try { + core.info( + `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` + ); + let javaArchivePath = await tc.downloadTool(javaRelease.url); - core.info(`Extracting Java archive...`); - const extension = getDownloadArchiveExtension(); - if (IS_WINDOWS) { - javaArchivePath = renameWinArchive(javaArchivePath); + core.info(`Extracting Java archive...`); + const extension = getDownloadArchiveExtension(); + if (IS_WINDOWS) { + javaArchivePath = renameWinArchive(javaArchivePath); + } + + const extractedJavaPath = await extractJdkFile( + javaArchivePath, + extension + ); + + // Add validation for extracted path + if (!fs.existsSync(extractedJavaPath)) { + throw new Error( + `Extraction failed: path ${extractedJavaPath} does not exist` + ); + } + + const dirContents = fs.readdirSync(extractedJavaPath); + if (dirContents.length === 0) { + throw new Error( + 'Extraction failed: no files found in extracted directory' + ); + } + + const archivePath = path.join(extractedJavaPath, dirContents[0]); + const version = this.getToolcacheVersionName(javaRelease.version); + + const javaPath = await tc.cacheDir( + archivePath, + this.toolcacheFolderName, + version, + this.architecture + ); + + return {version: javaRelease.version, path: javaPath}; + } catch (error) { + core.error(`Failed to download and extract GraalVM: ${error}`); + throw error; } - const extractedJavaPath = await extractJdkFile(javaArchivePath, extension); - const archivePath = path.join( - extractedJavaPath, - fs.readdirSync(extractedJavaPath)[0] - ); - const version = this.getToolcacheVersionName(javaRelease.version); - - const javaPath = await tc.cacheDir( - archivePath, - this.toolcacheFolderName, - version, - this.architecture - ); - return {version: javaRelease.version, path: javaPath}; } protected async findPackageForDownload( range: string ): Promise { + // Add input validation + if (!range || typeof range !== 'string') { + throw new Error('Version range is required and must be a string'); + } + const arch = this.distributionArchitecture(); - if (!['x64', 'aarch64'].includes(arch)) { - throw new Error(`Unsupported architecture: ${this.architecture}`); + if (!SUPPORTED_ARCHITECTURES.includes(arch as SupportedArchitecture)) { + throw new Error( + `Unsupported architecture: ${this.architecture}. Supported architectures are: ${SUPPORTED_ARCHITECTURES.join(', ')}` + ); } if (!this.stable) { @@ -74,6 +107,18 @@ export class GraalVMDistribution extends JavaBase { const platform = this.getPlatform(); const extension = getDownloadArchiveExtension(); const major = range.includes('.') ? range.split('.')[0] : range; + const majorVersion = parseInt(major); + + if (isNaN(majorVersion)) { + throw new Error(`Invalid version format: ${range}`); + } + + if (majorVersion < GRAALVM_MIN_VERSION) { + throw new Error( + `GraalVM is only supported for JDK ${GRAALVM_MIN_VERSION} and later. Requested version: ${major}` + ); + } + const fileUrl = this.constructFileUrl( range, major, @@ -82,10 +127,6 @@ export class GraalVMDistribution extends JavaBase { extension ); - if (parseInt(major) < 17) { - throw new Error('GraalVM is only supported for JDK 17 and later'); - } - const response = await this.http.head(fileUrl); this.handleHttpResponse(response, range); @@ -105,12 +146,26 @@ export class GraalVMDistribution extends JavaBase { } private handleHttpResponse(response: any, range: string): void { - if (response.message.statusCode === HttpCodes.NotFound) { - throw new Error(`Could not find GraalVM for SemVer ${range}`); - } - if (response.message.statusCode !== HttpCodes.OK) { + const statusCode = response.message.statusCode; + + if (statusCode === HttpCodes.NotFound) { throw new Error( - `Http request for GraalVM failed with status code: ${response.message.statusCode}` + `Could not find GraalVM for SemVer ${range}. Please check if this version is available at ${GRAALVM_DL_BASE}` + ); + } + + if ( + statusCode === HttpCodes.Unauthorized || + statusCode === HttpCodes.Forbidden + ) { + throw new Error( + `Access denied when downloading GraalVM. Status code: ${statusCode}. Please check your credentials or permissions.` + ); + } + + if (statusCode !== HttpCodes.OK) { + throw new Error( + `HTTP request for GraalVM failed with status code: ${statusCode} (${response.message.statusMessage || 'Unknown error'})` ); } } @@ -118,22 +173,46 @@ export class GraalVMDistribution extends JavaBase { private async findEABuildDownloadUrl( javaEaVersion: string ): Promise { + core.debug(`Searching for EA build: ${javaEaVersion}`); + const versions = await this.fetchEAJson(javaEaVersion); + core.debug(`Found ${versions.length} EA versions`); + const latestVersion = versions.find(v => v.latest); if (!latestVersion) { + core.error( + `Available versions: ${versions.map(v => v.version).join(', ')}` + ); throw new Error(`Unable to find latest version for '${javaEaVersion}'`); } + core.debug(`Latest version found: ${latestVersion.version}`); + const arch = this.distributionArchitecture(); const file = latestVersion.files.find( f => f.arch === arch && f.platform === GRAALVM_PLATFORM ); - if (!file || !file.filename.startsWith('graalvm-jdk-')) { - throw new Error(`Unable to find file metadata for '${javaEaVersion}'`); + + if (!file) { + core.error( + `Available files for architecture ${arch}: ${JSON.stringify(latestVersion.files)}` + ); + throw new Error( + `Unable to find file for architecture '${arch}' and platform '${GRAALVM_PLATFORM}'` + ); } + if (!file.filename.startsWith('graalvm-jdk-')) { + throw new Error( + `Invalid filename format: ${file.filename}. Expected to start with 'graalvm-jdk-'` + ); + } + + const downloadUrl = `${latestVersion.download_base_url}${file.filename}`; + core.debug(`Download URL: ${downloadUrl}`); + return { - url: `${latestVersion.download_base_url}${file.filename}`, + url: downloadUrl, version: latestVersion.version }; } @@ -147,16 +226,38 @@ export class GraalVMDistribution extends JavaBase { core.debug( `Trying to fetch available version info for GraalVM EA builds from '${url}'` ); - const fetchedJson = await this.http - .getJson(url, headers) - .then(res => res.result); - if (!fetchedJson) { + try { + const response = await this.http.getJson( + url, + headers + ); + + if (!response.result) { + throw new Error( + `No GraalVM EA build found for version '${javaEaVersion}'. Please check if the version is correct.` + ); + } + + return response.result; + } catch (error) { + if (error instanceof Error) { + // Check if it's a 404 error (file not found) + if (error.message?.includes('404')) { + throw new Error( + `GraalVM EA version '${javaEaVersion}' not found. Please verify the version exists in the EA builds repository.` + ); + } + // Re-throw with more context + throw new Error( + `Failed to fetch GraalVM EA version information for '${javaEaVersion}': ${error.message}` + ); + } + // If it's not an Error instance, throw a generic error throw new Error( - `No GraalVM EA build found for version '${javaEaVersion}'. Please check if the version is correct.` + `Failed to fetch GraalVM EA version information for '${javaEaVersion}'` ); } - return fetchedJson; } public getPlatform(platform: NodeJS.Platform = process.platform): OsVersions {