diff --git a/README.md b/README.md index ecee210..72533a3 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ the [check-latest input](docs/advanced-usage.md#check-latest-version). The `go-version` input supports the following syntax: - Specific versions: `1.25`, `1.24.11`, `1.24.0-rc.1`, `1.23.0-beta.1` +- Native Go versions: `go1.21`, `go1.21.5`, `go1.18beta2`, `go1.21rc2`. `go1.21` resolves to exactly `1.21.0`, unlike `1.21`, which matches patch releases. - SemVer version range syntax: `^1.25.1`, `~1.24.1`, `>=1.25.0-rc.1`, `<1.25.0`, `>=1.22.0 <1.24.0` - Aliases: `stable`, `oldstable` - Wildcards: `1.25.x`, `1.x` diff --git a/__tests__/setup-go.test.ts b/__tests__/setup-go.test.ts index c1946bd..ac62616 100644 --- a/__tests__/setup-go.test.ts +++ b/__tests__/setup-go.test.ts @@ -866,6 +866,11 @@ describe('setup-go', () => { expect(im.makeSemver('1.10rc1')).toBe('1.10.0-rc.1'); }); + it('preserves already-normalized prerelease versions', () => { + expect(im.makeSemver('1.18.0-beta.2')).toBe('1.18.0-beta.2'); + expect(im.makeSemver('1.21.0-rc.2')).toBe('1.21.0-rc.2'); + }); + it('converts dot zero versions', async () => { expect(im.makeSemver('1.13')).toBe('1.13.0'); }); @@ -874,6 +879,102 @@ describe('setup-go', () => { expect(im.makeSemver('1.13.1')).toBe('1.13.1'); }); + it('isGoNativeVersion accepts supported Go-native version formats', () => { + expect(im.isGoNativeVersion('go1.21')).toBe(true); + expect(im.isGoNativeVersion('go1.21.5')).toBe(true); + expect(im.isGoNativeVersion('go1.18beta2')).toBe(true); + expect(im.isGoNativeVersion('go1.21rc2')).toBe(true); + + expect(im.isGoNativeVersion('go1.21.x')).toBe(false); + expect(im.isGoNativeVersion('go^1.21')).toBe(false); + expect(im.isGoNativeVersion('1.21')).toBe(false); + expect(im.isGoNativeVersion('go1')).toBe(false); + expect(im.isGoNativeVersion('go1.21.5beta1')).toBe(false); + expect(im.isGoNativeVersion('go1.21.0rc1')).toBe(false); + }); + + it('accepts a Go-native go-version input and normalizes it', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + inputs['go-version'] = 'go1.21'; + findSpy.mockImplementation(() => path.normalize('/cache/go/1.21.0/x64')); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith('Setup go version spec go1.21'); + expect(findSpy).toHaveBeenCalledWith('go', '1.21.0', 'x64'); + expect(logSpy).toHaveBeenCalledWith( + 'Successfully set up Go version go1.21' + ); + }); + + it('rejects a malformed Go-native go-version input', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + inputs['go-version'] = 'go1.21.x'; + + await main.run(); + + expect(cnSpy).toHaveBeenCalledWith( + expect.stringContaining('Invalid Go version "go1.21.x"') + ); + }); + + it('accepts a Go-native go-version input with beta suffix and normalizes it', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + inputs['go-version'] = 'go1.18beta2'; + findSpy.mockImplementation(() => + path.normalize('/cache/go/1.18.0-beta.2/x64') + ); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith('Setup go version spec go1.18beta2'); + expect(findSpy).toHaveBeenCalledWith('go', '1.18.0-beta.2', 'x64'); + expect(logSpy).toHaveBeenCalledWith( + 'Successfully set up Go version go1.18beta2' + ); + }); + + it('accepts a Go-native go-version input with rc suffix and normalizes it', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + inputs['go-version'] = 'go1.21rc2'; + findSpy.mockImplementation(() => + path.normalize('/cache/go/1.21.0-rc.2/x64') + ); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith('Setup go version spec go1.21rc2'); + expect(findSpy).toHaveBeenCalledWith('go', '1.21.0-rc.2', 'x64'); + expect(logSpy).toHaveBeenCalledWith( + 'Successfully set up Go version go1.21rc2' + ); + }); + + it('downloads a normalized Go-native version on cache miss', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + inputs['go-version'] = 'go1.13'; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => + path.normalize('/cache/go/1.13.0/x64') + ); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith('Attempting to download 1.13.0...'); + expect(dlSpy).toHaveBeenCalled(); + expect(logSpy).toHaveBeenCalledWith( + 'Successfully set up Go version go1.13' + ); + }); + describe('check-latest flag', () => { it("use local version and don't check manifest if check-latest is not specified", async () => { os.platform = 'linux'; @@ -1297,6 +1398,43 @@ use . }); describe('go-download-base-url', () => { + it('normalizes native versions but preserves custom download filenames', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = 'go1.21rc2'; + const customBaseUrl = 'https://example.com/golang'; + const toolName = im.customToolCacheName(customBaseUrl); + const toolPath = path.normalize(`/cache/${toolName}/1.21.0-rc.2/x64`); + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + + getSpy.mockImplementationOnce(() => { + throw new Error('Not a JSON endpoint'); + }); + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + execFileSpy.mockImplementation(() => 'go version go1.21rc2 linux/amd64'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + expect(findSpy).toHaveBeenCalledWith(toolName, '1.21.0-rc.2', 'x64'); + expect(getSpy).toHaveBeenCalledWith( + `${customBaseUrl}/?mode=json&include=all` + ); + expect(dlSpy).toHaveBeenCalledWith( + `${customBaseUrl}/go1.21rc2.linux-amd64.tar.gz`, + undefined, + undefined + ); + expect(cnSpy).toHaveBeenCalledWith( + `::add-path::${path.join(toolPath, 'bin')}${osm.EOL}` + ); + }); + it('downloads a version from custom base URL using version listing', async () => { os.platform = 'linux'; os.arch = 'x64'; diff --git a/action.yml b/action.yml index 79299e5..e66304c 100644 --- a/action.yml +++ b/action.yml @@ -3,7 +3,7 @@ description: 'Setup a Go environment and add it to the PATH' author: 'GitHub' inputs: go-version: - description: 'The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks.' + description: 'The Go version to download (if necessary) and use. Supports native Go versions (e.g. go1.21.5), semver specs and ranges. Be sure to enclose this option in single quotation marks.' go-version-file: description: 'Path to the go.mod, go.work, .go-version, or .tool-versions file.' check-latest: diff --git a/dist/setup/index.js b/dist/setup/index.js index e8bba74..6bb75f0 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -41969,6 +41969,10 @@ async function getGo(versionSpec, checkLatest, auth, arch = external_os_default( let manifest; const osPlat = external_os_default().platform(); const customBaseUrl = goDownloadBaseUrl?.replace(/\/+$/, ''); + const originalVersionSpec = versionSpec; + if (isGoNativeVersion(versionSpec)) { + versionSpec = makeSemver(versionSpec); + } if (versionSpec === StableReleaseAlias.Stable || versionSpec === StableReleaseAlias.OldStable) { if (customBaseUrl) { @@ -42023,7 +42027,7 @@ async function getGo(versionSpec, checkLatest, auth, arch = external_os_default( const skipVersionListing = NO_VERSION_LISTING_BASE_URLS.some(url => customBaseUrl.toLowerCase() === url.toLowerCase()); if (skipVersionListing) { core_info('Skipping version listing for known direct-download URL. Constructing download URL directly.'); - info = getInfoFromDirectDownload(versionSpec, arch, customBaseUrl); + info = getInfoFromDirectDownload(originalVersionSpec, arch, customBaseUrl); } else { try { @@ -42033,7 +42037,7 @@ async function getGo(versionSpec, checkLatest, auth, arch = external_os_default( core_info('Version listing not available from custom URL. Constructing download URL directly.'); } if (!info) { - info = getInfoFromDirectDownload(versionSpec, arch, customBaseUrl); + info = getInfoFromDirectDownload(originalVersionSpec, arch, customBaseUrl); } } try { @@ -42043,10 +42047,10 @@ async function getGo(versionSpec, checkLatest, auth, arch = external_os_default( catch (err) { const downloadUrl = info?.downloadUrl || customBaseUrl; if (err instanceof HTTPError && err.httpStatusCode === 404) { - throw new Error(`The requested Go version ${versionSpec} is not available for platform ${osPlat}/${arch}. ` + + throw new Error(`The requested Go version ${originalVersionSpec} is not available for platform ${osPlat}/${arch}. ` + `Download URL returned HTTP 404: ${downloadUrl}`, { cause: err }); } - throw new Error(`Failed to download Go ${versionSpec} for platform ${osPlat}/${arch} ` + + throw new Error(`Failed to download Go ${originalVersionSpec} for platform ${osPlat}/${arch} ` + `from ${downloadUrl}: ${err}`, { cause: err }); } } @@ -42335,6 +42339,10 @@ async function findMatch(versionSpec, arch = external_os_default().arch(), dlUrl } return result; } +function isGoNativeVersion(version) { + const expression = /^go[1-9]\d*\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*)|(?:beta|rc)[1-9]\d*)?$/; + return expression.test(version); +} // // Convert the go version syntax into semver for semver matching // 1.13.1 => 1.13.1 @@ -42342,6 +42350,10 @@ async function findMatch(versionSpec, arch = external_os_default().arch(), dlUrl // 1.10beta1 => 1.10.0-beta.1, 1.10rc1 => 1.10.0-rc.1 // 1.8.5beta1 => 1.8.5-beta.1, 1.8.5rc1 => 1.8.5-rc.1 function makeSemver(version) { + const validVersion = node_modules_semver.valid(version); + if (validVersion) { + return validVersion; + } version = version.replace('go', ''); version = version.replace('beta', '-beta.').replace('rc', '-rc.'); const parts = version.split('-'); @@ -97835,6 +97847,13 @@ function resolveVersionInput() { warning('Both go-version and go-version-file inputs are specified, only go-version will be used'); } if (version) { + const customBaseUrl = getInput('go-download-base-url') || + process.env['GO_DOWNLOAD_BASE_URL']; + if (version.startsWith('go') && + !customBaseUrl && + !isGoNativeVersion(version)) { + throw new Error(`Invalid Go version "${version}". Go-native versions must look like go1.21, go1.21.5, go1.18beta2, or go1.21rc2. For version ranges, use SemVer syntax (e.g. ~1.21, ^1.21).`); + } return version; } if (versionFilePath) { diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 0e3efa5..5cc2543 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -50,9 +50,13 @@ steps: - The preinstalled patch version is generally the latest available. When a new patch is released, it replaces the previously preinstalled version on the runner. - Using the most recent patch version speeds up setup because the required Go version is already installed on the runner and no download is needed. +The `go-version` input also accepts native Go version strings: `go1.21.5`, `go1.18beta2`, and `go1.21rc2` normalize to `1.21.5`, `1.18.0-beta.2`, and `1.21.0-rc.2`, respectively. `go1.21` resolves to exactly `1.21.0`; use `1.21` for matching patch releases. For ranges and wildcards, use SemVer syntax such as `~1.21` or `1.21.x`, not `go1.21.x`. + To try a **pre-release**: Download and use beta or RC Go versions as needed. +The `go-version` input also accepts native Go version strings: `go1.21.5`, `go1.18beta2`, and `go1.21rc2` normalize to `1.21.5`, `1.18.0-beta.2`, and `1.21.0-rc.2`, respectively. `go1.21` resolves to exactly `1.21.0`; use `1.21` for matching patch releases. For ranges and wildcards, use SemVer syntax such as `~1.21` or `1.21.x`, not `go1.21.x`. + ```yaml # RC version steps: @@ -459,6 +463,8 @@ The `go-download-base-url` input lets you download Go from a mirror or alternati When a custom base URL is provided, the action skips the `actions/go-versions` manifest lookup and downloads directly from the specified URL. +> **Note:** When constructing direct-download filenames, the original version string is preserved; native Go version normalization is used only for matching and tool cache lookups. + **Using the [Microsoft build of Go](https://github.com/microsoft/go):** ```yaml diff --git a/src/installer.ts b/src/installer.ts index 1354d7b..a497bfd 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -60,6 +60,11 @@ export async function getGo( const osPlat: string = os.platform(); const customBaseUrl = goDownloadBaseUrl?.replace(/\/+$/, ''); + const originalVersionSpec = versionSpec; + if (isGoNativeVersion(versionSpec)) { + versionSpec = makeSemver(versionSpec); + } + if ( versionSpec === StableReleaseAlias.Stable || versionSpec === StableReleaseAlias.OldStable @@ -146,7 +151,11 @@ export async function getGo( core.info( 'Skipping version listing for known direct-download URL. Constructing download URL directly.' ); - info = getInfoFromDirectDownload(versionSpec, arch, customBaseUrl); + info = getInfoFromDirectDownload( + originalVersionSpec, + arch, + customBaseUrl + ); } else { try { info = await getInfoFromDist(versionSpec, arch, customBaseUrl); @@ -156,7 +165,11 @@ export async function getGo( ); } if (!info) { - info = getInfoFromDirectDownload(versionSpec, arch, customBaseUrl); + info = getInfoFromDirectDownload( + originalVersionSpec, + arch, + customBaseUrl + ); } } @@ -167,13 +180,13 @@ export async function getGo( 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}. ` + + `The requested Go version ${originalVersionSpec} is not available for platform ${osPlat}/${arch}. ` + `Download URL returned HTTP 404: ${downloadUrl}`, {cause: err} ); } throw new Error( - `Failed to download Go ${versionSpec} for platform ${osPlat}/${arch} ` + + `Failed to download Go ${originalVersionSpec} for platform ${osPlat}/${arch} ` + `from ${downloadUrl}: ${err}`, {cause: err} ); @@ -610,6 +623,12 @@ export async function findMatch( return result; } +export function isGoNativeVersion(version: string): boolean { + const expression = + /^go[1-9]\d*\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*)|(?:beta|rc)[1-9]\d*)?$/; + return expression.test(version); +} + // // Convert the go version syntax into semver for semver matching // 1.13.1 => 1.13.1 @@ -617,6 +636,10 @@ export async function findMatch( // 1.10beta1 => 1.10.0-beta.1, 1.10rc1 => 1.10.0-rc.1 // 1.8.5beta1 => 1.8.5-beta.1, 1.8.5rc1 => 1.8.5-rc.1 export function makeSemver(version: string): string { + const validVersion = semver.valid(version); + if (validVersion) { + return validVersion; + } version = version.replace('go', ''); version = version.replace('beta', '-beta.').replace('rc', '-rc.'); const parts = version.split('-'); diff --git a/src/main.ts b/src/main.ts index b1c9a49..56a2706 100644 --- a/src/main.ts +++ b/src/main.ts @@ -215,6 +215,18 @@ function resolveVersionInput(): string { } if (version) { + const customBaseUrl = + core.getInput('go-download-base-url') || + process.env['GO_DOWNLOAD_BASE_URL']; + if ( + version.startsWith('go') && + !customBaseUrl && + !installer.isGoNativeVersion(version) + ) { + throw new Error( + `Invalid Go version "${version}". Go-native versions must look like go1.21, go1.21.5, go1.18beta2, or go1.21rc2. For version ranges, use SemVer syntax (e.g. ~1.21, ^1.21).` + ); + } return version; }