diff --git a/__tests__/setup-go.test.ts b/__tests__/setup-go.test.ts index f83d6a3..6b04284 100644 --- a/__tests__/setup-go.test.ts +++ b/__tests__/setup-go.test.ts @@ -45,6 +45,7 @@ describe('setup-go', () => { let mkdirSpy: jest.SpyInstance; let symlinkSpy: jest.SpyInstance; let execSpy: jest.SpyInstance; + let execFileSpy: jest.SpyInstance; let getManifestSpy: jest.SpyInstance; let getAllVersionsSpy: jest.SpyInstance; let httpmGetJsonSpy: jest.SpyInstance; @@ -71,6 +72,10 @@ describe('setup-go', () => { archSpy = jest.spyOn(osm, 'arch'); archSpy.mockImplementation(() => os['arch']); execSpy = jest.spyOn(cp, 'execSync'); + execFileSpy = jest.spyOn(cp, 'execFileSync'); + execFileSpy.mockImplementation(() => { + throw new Error('ENOENT'); + }); // switch path join behaviour based on set os.platform joinSpy = jest.spyOn(path, 'join'); @@ -1393,5 +1398,178 @@ use . ); expect(info.fileName).toBe('go1.25.0.darwin-arm64.tar.gz'); }); + + it('caches under actual installed version when it differs from input spec', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.20'; + const customBaseUrl = 'https://aka.ms/golang/release/latest'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + + // Simulate JSON API not being available (like aka.ms) + getSpy.mockImplementationOnce(() => { + throw new Error('Not a JSON endpoint'); + }); + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + + // Mock the installed Go binary reporting a different patch version + execFileSpy.mockImplementation(() => 'go version go1.20.14 linux/amd64'); + + const toolPath = path.normalize('/cache/go-custom/1.20.14/x64'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith( + "Requested version '1.20' resolved to installed version '1.20.14'" + ); + // Cache key should use actual version, not the input spec + expect(cacheSpy).toHaveBeenCalledWith( + expect.any(String), + 'go-custom', + '1.20.14', + 'x64' + ); + }); + + it('shows clear error with platform/arch and URL on 404', async () => { + os.platform = 'linux'; + os.arch = 'arm64'; + + const versionSpec = '1.25.0'; + const customBaseUrl = 'https://example.com/golang'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + + getSpy.mockImplementationOnce(() => { + throw new Error('Not a JSON endpoint'); + }); + + findSpy.mockImplementation(() => ''); + const httpError = new tc.HTTPError(404); + dlSpy.mockImplementation(() => { + throw httpError; + }); + + await main.run(); + + expect(cnSpy).toHaveBeenCalledWith( + expect.stringContaining( + 'The requested Go version 1.25.0 is not available for platform linux/arm64' + ) + ); + expect(cnSpy).toHaveBeenCalledWith( + expect.stringContaining('HTTP 404') + ); + }); + + it('shows clear error with platform/arch and URL on download failure', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.25.0'; + const customBaseUrl = 'https://example.com/golang'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + + getSpy.mockImplementationOnce(() => { + throw new Error('Not a JSON endpoint'); + }); + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(() => { + throw new Error('connection refused'); + }); + + await main.run(); + + expect(cnSpy).toHaveBeenCalledWith( + expect.stringContaining( + 'Failed to download Go 1.25.0 for platform linux/x64' + ) + ); + expect(cnSpy).toHaveBeenCalledWith( + expect.stringContaining(customBaseUrl) + ); + }); + + it.each(['^1.25.0', '~1.25', '>=1.25.0', '<1.26.0', '1.25.x', '1.x'])( + 'errors on version range "%s" when version listing is unavailable', + async versionSpec => { + os.platform = 'linux'; + os.arch = 'x64'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = 'https://example.com/golang'; + + // Simulate version listing not available + getSpy.mockImplementationOnce(() => { + throw new Error('Not a JSON endpoint'); + }); + + findSpy.mockImplementation(() => ''); + + await main.run(); + + expect(cnSpy).toHaveBeenCalledWith( + expect.stringContaining( + `Version range '${versionSpec}' is not supported with a custom download base URL` + ) + ); + } + ); + + it('rejects version range in getInfoFromDirectDownload', () => { + os.platform = 'linux'; + os.arch = 'x64'; + + expect(() => + im.getInfoFromDirectDownload( + '^1.25.0', + 'x64', + 'https://example.com/golang' + ) + ).toThrow( + "Version range '^1.25.0' is not supported with a custom download base URL" + ); + }); + + it('passes token as auth header for custom URL downloads', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.25.0'; + const customBaseUrl = 'https://private-mirror.example.com/golang'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + inputs['token'] = 'ghp_testtoken123'; + + getSpy.mockImplementationOnce(() => { + throw new Error('Not a JSON endpoint'); + }); + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + const toolPath = path.normalize('/cache/go-custom/1.25.0/x64'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + expect(dlSpy).toHaveBeenCalledWith( + `${customBaseUrl}/go1.25.0.linux-amd64.tar.gz`, + undefined, + 'token ghp_testtoken123' + ); + }); }); }); diff --git a/dist/setup/index.js b/dist/setup/index.js index 39d5e8d..f7c501a 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -49577,6 +49577,7 @@ const path = __importStar(__nccwpck_require__(16928)); const semver = __importStar(__nccwpck_require__(62088)); const httpm = __importStar(__nccwpck_require__(54844)); const sys = __importStar(__nccwpck_require__(57666)); +const child_process_1 = __importDefault(__nccwpck_require__(35317)); const fs_1 = __importDefault(__nccwpck_require__(79896)); const os_1 = __importDefault(__nccwpck_require__(70857)); const utils_1 = __nccwpck_require__(71798); @@ -49644,7 +49645,6 @@ function getGo(versionSpec_1, checkLatest_1, auth_1) { // // Download from custom base URL // - core.info(`Using custom download base URL: ${customBaseUrl}`); try { info = yield getInfoFromDist(versionSpec, arch, customBaseUrl); } @@ -49656,10 +49656,16 @@ function getGo(versionSpec_1, checkLatest_1, auth_1) { } try { core.info('Install from custom download URL'); - downloadPath = yield installGoVersion(info, undefined, arch, toolCacheName); + downloadPath = yield installGoVersion(info, auth, arch, toolCacheName); } catch (err) { - throw new Error(`Failed to download version ${versionSpec}: ${err}`); + const downloadUrl = (info === null || info === void 0 ? void 0 : info.downloadUrl) || customBaseUrl; + if (err instanceof tc.HTTPError && err.httpStatusCode === 404) { + throw new Error(`The requested Go version ${versionSpec} is not available for platform ${osPlat}/${arch}. ` + + `Download URL returned HTTP 404: ${downloadUrl}`); + } + throw new Error(`Failed to download Go ${versionSpec} for platform ${osPlat}/${arch} ` + + `from ${downloadUrl}: ${err}`); } } else { @@ -49774,12 +49780,33 @@ function installGoVersion(info_1, auth_1, arch_1) { if (info.type === 'dist') { extPath = path.join(extPath, 'go'); } + // For custom downloads, detect the actual installed version so the cache + // key reflects the real patch level (e.g. input "1.20" may install 1.20.14). + if (toolName !== 'go') { + const actualVersion = detectInstalledGoVersion(extPath); + if (actualVersion && actualVersion !== info.resolvedVersion) { + core.info(`Requested version '${info.resolvedVersion}' resolved to installed version '${actualVersion}'`); + info.resolvedVersion = actualVersion; + } + } core.info('Adding to the cache ...'); const toolCacheDir = yield addExecutablesToToolCache(extPath, info, arch, toolName); core.info(`Successfully cached go to ${toolCacheDir}`); return toolCacheDir; }); } +function detectInstalledGoVersion(goDir) { + try { + const goBin = path.join(goDir, 'bin', os_1.default.platform() === 'win32' ? 'go.exe' : 'go'); + const output = child_process_1.default.execFileSync(goBin, ['version'], { encoding: 'utf8' }); + const match = output.match(/go version go(\S+)/); + return match ? match[1] : null; + } + catch (err) { + core.debug(`Failed to detect installed Go version: ${err.message}`); + return null; + } +} function extractGoArchive(archivePath) { return __awaiter(this, void 0, void 0, function* () { const platform = os_1.default.platform(); @@ -49881,6 +49908,11 @@ function getInfoFromDist(versionSpec, arch, goDownloadBaseUrl) { }); } function getInfoFromDirectDownload(versionSpec, arch, goDownloadBaseUrl) { + // Reject version specs that can't map to an artifact filename + if (/[~^>=<|*x]/.test(versionSpec)) { + throw new Error(`Version range '${versionSpec}' is not supported with a custom download base URL ` + + `when version listing is unavailable. Please specify an exact version (e.g., '1.25.0').`); + } const archStr = sys.getArch(arch); const platStr = sys.getPlatform(); const extension = platStr === 'windows' ? 'zip' : 'tar.gz'; diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 6b0272c..1ca3810 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -223,6 +223,8 @@ want the most up-to-date Go version to always be used. It supports major (e.g., > Setting `check-latest` to `true` has performance implications as downloading Go versions is slower than using cached > versions. +> +> `check-latest` is ignored when `go-download-base-url` is set. See [Custom download URL](#custom-download-url) for details. ```yaml steps: @@ -450,7 +452,24 @@ steps: - run: go version ``` -> **Note:** Version range syntax (`^1.25`, `~1.24`) and aliases (`stable`, `oldstable`) are not supported with custom download URLs. Use specific versions (e.g., `1.25` or `1.25.0`) instead. +> **Note:** Version range syntax (`^1.25`, `~1.24`, `>=1.25.0`) and aliases (`stable`, `oldstable`) are not supported with custom download URLs. Use exact versions such as `1.25`, `1.25.0`, or `1.25.0-1` (for sources that use revision numbers). If the custom server provides a version listing endpoint (`/?mode=json&include=all`), semver ranges will work; otherwise only exact versions are accepted. + +> **Note:** The `check-latest` option is ignored when a custom download base URL is set. The action cannot query the custom server for the latest version, so it uses the version you specify directly. If you provide a partial version like `1.25`, the server determines which patch release to serve. + +**Authenticated downloads:** + +If your custom download source requires authentication, the `token` input is forwarded as an `Authorization` header. For example, to download from a private mirror: + +```yaml +steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: '1.25' + go-download-base-url: 'https://private-mirror.example.com/golang' + token: ${{ secrets.MIRROR_TOKEN }} + - run: go version +``` ## Using `setup-go` on GHES diff --git a/src/installer.ts b/src/installer.ts index d8fbe62..e080642 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import * as semver from 'semver'; import * as httpm from '@actions/http-client'; import * as sys from './system'; +import cp from 'child_process'; import fs from 'fs'; import os from 'os'; import {StableReleaseAlias, isSelfHosted} from './utils'; @@ -129,7 +130,6 @@ export async function getGo( // // Download from custom base URL // - core.info(`Using custom download base URL: ${customBaseUrl}`); try { info = await getInfoFromDist(versionSpec, arch, customBaseUrl); } catch { @@ -145,12 +145,22 @@ export async function getGo( core.info('Install from custom download URL'); downloadPath = await installGoVersion( info, - undefined, + auth, arch, toolCacheName ); } catch (err) { - throw new Error(`Failed to download version ${versionSpec}: ${err}`); + const downloadUrl = info?.downloadUrl || customBaseUrl; + if (err instanceof tc.HTTPError && err.httpStatusCode === 404) { + throw new Error( + `The requested Go version ${versionSpec} is not available for platform ${osPlat}/${arch}. ` + + `Download URL returned HTTP 404: ${downloadUrl}` + ); + } + throw new Error( + `Failed to download Go ${versionSpec} for platform ${osPlat}/${arch} ` + + `from ${downloadUrl}: ${err}` + ); } } else { // @@ -312,6 +322,18 @@ async function installGoVersion( extPath = path.join(extPath, 'go'); } + // For custom downloads, detect the actual installed version so the cache + // key reflects the real patch level (e.g. input "1.20" may install 1.20.14). + if (toolName !== 'go') { + const actualVersion = detectInstalledGoVersion(extPath); + if (actualVersion && actualVersion !== info.resolvedVersion) { + core.info( + `Requested version '${info.resolvedVersion}' resolved to installed version '${actualVersion}'` + ); + info.resolvedVersion = actualVersion; + } + } + core.info('Adding to the cache ...'); const toolCacheDir = await addExecutablesToToolCache( extPath, @@ -324,6 +346,24 @@ async function installGoVersion( return toolCacheDir; } +function detectInstalledGoVersion(goDir: string): string | null { + try { + const goBin = path.join( + goDir, + 'bin', + os.platform() === 'win32' ? 'go.exe' : 'go' + ); + const output = cp.execFileSync(goBin, ['version'], {encoding: 'utf8'}); + const match = output.match(/go version go(\S+)/); + return match ? match[1] : null; + } catch (err) { + core.debug( + `Failed to detect installed Go version: ${(err as Error).message}` + ); + return null; + } +} + export async function extractGoArchive(archivePath: string): Promise { const platform = os.platform(); let extPath: string; @@ -469,6 +509,14 @@ export function getInfoFromDirectDownload( arch: Architecture, goDownloadBaseUrl: string ): IGoVersionInfo { + // Reject version specs that can't map to an artifact filename + if (/[~^>=<|*x]/.test(versionSpec)) { + throw new Error( + `Version range '${versionSpec}' is not supported with a custom download base URL ` + + `when version listing is unavailable. Please specify an exact version (e.g., '1.25.0').` + ); + } + const archStr = sys.getArch(arch); const platStr = sys.getPlatform(); const extension = platStr === 'windows' ? 'zip' : 'tar.gz';