diff --git a/CHANGELOG.md b/CHANGELOG.md index 54672d2..b14b349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [4.4.0] - 2025-10-29 + +### Added + +- Add fallback URL support via optional `downloadBaseURLFallback` input for improved download reliability + ## [4.3.1] - 2025-08-12 ### Changed diff --git a/README.md b/README.md index 517f4c3..40087d3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Install a specific version of helm binary on the runner. Acceptable values are latest or any semantic version string like v3.5.0 Use this action in workflow to define which version of helm will be used. v2+ of this action only support Helm3. ```yaml -- uses: azure/setup-helm@v4.3.0 +- uses: azure/setup-helm@v4.4.0 with: version: '' # default is latest (stable) id: install diff --git a/action.yml b/action.yml index b67b5fb..4df0392 100644 --- a/action.yml +++ b/action.yml @@ -14,6 +14,10 @@ inputs: description: 'Set the download base URL' required: false default: 'https://get.helm.sh' + downloadBaseURLFallback: + description: 'Fallback base URL if primary download fails' + required: false + default: '' outputs: helm-path: description: 'Path to the cached helm binary' diff --git a/package.json b/package.json index 59338e2..c1b1b6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "setuphelm", - "version": "4.3.1", + "version": "4.4.0", "private": true, "description": "Setup helm", "author": "Anumita Shenoy", diff --git a/src/run.test.ts b/src/run.test.ts index 6e10689..08e35a4 100644 --- a/src/run.test.ts +++ b/src/run.test.ts @@ -228,7 +228,7 @@ describe('run.ts', () => { return {isDirectory: () => isDirectory} as fs.Stats }) - expect(await run.downloadHelm(downloadBaseURL, 'v4.0.0')).toBe( + expect(await run.downloadHelm(downloadBaseURL, 'v4.0.0', '')).toBe( path.join('pathToCachedDir', 'helm.exe') ) expect(toolCache.find).toHaveBeenCalledWith('helm', 'v4.0.0') @@ -252,9 +252,9 @@ describe('run.ts', () => { jest.spyOn(os, 'arch').mockReturnValue('x64') const downloadUrl = 'https://test.tld/helm-v3.2.1-windows-amd64.zip' - await expect(run.downloadHelm(downloadBaseURL, 'v3.2.1')).rejects.toThrow( - `Failed to download Helm from location ${downloadUrl}` - ) + await expect( + run.downloadHelm(downloadBaseURL, 'v3.2.1', '') + ).rejects.toThrow(`Failed to download Helm from location ${downloadUrl}`) expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.2.1') expect(toolCache.downloadTool).toHaveBeenCalledWith(`${downloadUrl}`) }) @@ -278,7 +278,7 @@ describe('run.ts', () => { return {isDirectory: () => isDirectory} as fs.Stats }) - expect(await run.downloadHelm(downloadBaseURL, 'v3.2.1')).toBe( + expect(await run.downloadHelm(downloadBaseURL, 'v3.2.1', '')).toBe( path.join('pathToCachedDir', 'helm.exe') ) expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.2.1') @@ -304,9 +304,9 @@ describe('run.ts', () => { return {isDirectory: () => isDirectory} as fs.Stats }) - await expect(run.downloadHelm(downloadBaseURL, 'v3.2.1')).rejects.toThrow( - 'Helm executable not found in path pathToCachedDir' - ) + await expect( + run.downloadHelm(downloadBaseURL, 'v3.2.1', '') + ).rejects.toThrow('Helm executable not found in path pathToCachedDir') expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.2.1') expect(toolCache.downloadTool).toHaveBeenCalledWith( 'https://test.tld/helm-v3.2.1-windows-amd64.zip' @@ -314,4 +314,86 @@ describe('run.ts', () => { expect(fs.chmodSync).toHaveBeenCalledWith('pathToTool', '777') expect(toolCache.extractZip).toHaveBeenCalledWith('pathToTool') }) + + test('downloadHelm() - use fallback URL when primary download fails', async () => { + const fallbackBaseURL = 'https://fallback.tld' + jest.spyOn(toolCache, 'find').mockReturnValue('') + jest + .spyOn(toolCache, 'downloadTool') + .mockRejectedValueOnce(new Error('Primary download failed')) + .mockResolvedValueOnce('pathToTool') + jest.spyOn(toolCache, 'extractZip').mockResolvedValue('extractedPath') + jest.spyOn(toolCache, 'cacheDir').mockResolvedValue('pathToCachedDir') + jest.spyOn(os, 'platform').mockReturnValue('win32') + jest.spyOn(os, 'arch').mockReturnValue('x64') + jest.spyOn(fs, 'chmodSync').mockImplementation() + jest.spyOn(core, 'warning').mockImplementation() + jest + .spyOn(fs, 'readdirSync') + .mockImplementation((file, _) => [ + 'helm.exe' as unknown as fs.Dirent> + ]) + jest.spyOn(fs, 'statSync').mockImplementation((file) => { + const isDirectory = + (file as string).indexOf('folder') == -1 ? false : true + return {isDirectory: () => isDirectory} as fs.Stats + }) + + expect( + await run.downloadHelm(downloadBaseURL, 'v3.2.1', fallbackBaseURL) + ).toBe(path.join('pathToCachedDir', 'helm.exe')) + expect(toolCache.downloadTool).toHaveBeenCalledTimes(2) + expect(toolCache.downloadTool).toHaveBeenNthCalledWith( + 1, + 'https://test.tld/helm-v3.2.1-windows-amd64.zip' + ) + expect(toolCache.downloadTool).toHaveBeenNthCalledWith( + 2, + 'https://fallback.tld/helm-v3.2.1-windows-amd64.zip' + ) + expect(core.warning).toHaveBeenCalled() + }) + + test('downloadHelm() - throw error if both primary and fallback downloads fail', async () => { + const fallbackBaseURL = 'https://fallback.tld' + jest.spyOn(toolCache, 'find').mockReturnValue('') + jest + .spyOn(toolCache, 'downloadTool') + .mockRejectedValueOnce(new Error('Primary download failed')) + .mockRejectedValueOnce(new Error('Fallback download failed')) + jest.spyOn(os, 'platform').mockReturnValue('win32') + jest.spyOn(os, 'arch').mockReturnValue('x64') + + await expect( + run.downloadHelm(downloadBaseURL, 'v3.2.1', fallbackBaseURL) + ).rejects.toThrow( + 'Failed to download Helm from location https://test.tld/helm-v3.2.1-windows-amd64.zip or https://fallback.tld/helm-v3.2.1-windows-amd64.zip' + ) + expect(toolCache.downloadTool).toHaveBeenCalledTimes(2) + }) + + test('downloadHelm() - work without fallback URL (backwards compatibility)', async () => { + jest.spyOn(toolCache, 'find').mockReturnValue('') + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool') + jest.spyOn(toolCache, 'extractZip').mockResolvedValue('extractedPath') + jest.spyOn(toolCache, 'cacheDir').mockResolvedValue('pathToCachedDir') + jest.spyOn(os, 'platform').mockReturnValue('win32') + jest.spyOn(os, 'arch').mockReturnValue('x64') + jest.spyOn(fs, 'chmodSync').mockImplementation() + jest + .spyOn(fs, 'readdirSync') + .mockImplementation((file, _) => [ + 'helm.exe' as unknown as fs.Dirent> + ]) + jest.spyOn(fs, 'statSync').mockImplementation((file) => { + const isDirectory = + (file as string).indexOf('folder') == -1 ? false : true + return {isDirectory: () => isDirectory} as fs.Stats + }) + + expect(await run.downloadHelm(downloadBaseURL, 'v3.2.1')).toBe( + path.join('pathToCachedDir', 'helm.exe') + ) + expect(toolCache.downloadTool).toHaveBeenCalledTimes(1) + }) }) diff --git a/src/run.ts b/src/run.ts index 7e30c13..90daa60 100644 --- a/src/run.ts +++ b/src/run.ts @@ -24,9 +24,16 @@ export async function run() { } const downloadBaseURL = core.getInput('downloadBaseURL', {required: false}) + const downloadBaseURLFallback = core.getInput('downloadBaseURLFallback', { + required: false + }) core.startGroup(`Installing ${version}`) - const cachedPath = await downloadHelm(downloadBaseURL, version) + const cachedPath = await downloadHelm( + downloadBaseURL, + version, + downloadBaseURLFallback + ) core.endGroup() try { @@ -84,7 +91,8 @@ export function getHelmDownloadURL(baseURL: string, version: string): string { export async function downloadHelm( baseURL: string, - version: string + version: string, + fallbackBaseURL?: string ): Promise { let cachedToolpath = toolCache.find(helmToolName, version) if (cachedToolpath) { @@ -97,12 +105,33 @@ export async function downloadHelm( getHelmDownloadURL(baseURL, version) ) } catch (exception) { - throw new Error( - `Failed to download Helm from location ${getHelmDownloadURL( - baseURL, - version - )}` - ) + if (fallbackBaseURL) { + core.warning( + `Failed to download Helm from location ${getHelmDownloadURL( + baseURL, + version + )}. Attempting to download from fallback URL: ${fallbackBaseURL}` + ) + try { + helmDownloadPath = await toolCache.downloadTool( + getHelmDownloadURL(fallbackBaseURL, version) + ) + } catch (fallbackException) { + throw new Error( + `Failed to download Helm from location ${getHelmDownloadURL( + baseURL, + version + )} or ${getHelmDownloadURL(fallbackBaseURL, version)}` + ) + } + } else { + throw new Error( + `Failed to download Helm from location ${getHelmDownloadURL( + baseURL, + version + )}` + ) + } } fs.chmodSync(helmDownloadPath, '777')