From e3bdf9de39bb4b4c0b3ad206af53f31bff23e22a Mon Sep 17 00:00:00 2001 From: Marcus Filho Date: Wed, 29 Oct 2025 16:32:39 -0300 Subject: [PATCH 1/2] including fallback url --- action.yml | 4 ++ src/run.test.ts | 98 +++++++++++++++++++++++++++++++++++++++++++++---- src/run.ts | 45 +++++++++++++++++++---- 3 files changed, 131 insertions(+), 16 deletions(-) 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/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') From 2da3d27d8f754f9bbd0e53e46943972d2c79cd25 Mon Sep 17 00:00:00 2001 From: Marcus Filho Date: Wed, 29 Oct 2025 16:34:18 -0300 Subject: [PATCH 2/2] bump version --- CHANGELOG.md | 6 ++++++ README.md | 2 +- package.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) 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/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",