diff --git a/.github/workflows/microsoft-validation.yml b/.github/workflows/microsoft-validation.yml new file mode 100644 index 0000000..76eb9ec --- /dev/null +++ b/.github/workflows/microsoft-validation.yml @@ -0,0 +1,143 @@ +name: Validate Microsoft build of Go + +on: + push: + branches: + - main + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + +jobs: + microsoft-basic: + name: 'Microsoft build of Go ${{ matrix.go-version }} on ${{ matrix.os }}' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + go-version: ['1.25', '1.24'] + steps: + - uses: actions/checkout@v6 + + - name: Setup Microsoft build of Go ${{ matrix.go-version }} + uses: ./ + with: + go-version: ${{ matrix.go-version }} + go-download-base-url: 'https://aka.ms/golang/release/latest' + cache: false + + - name: Verify Go installation + run: go version + + - name: Verify Go env + run: go env + + - name: Verify Go is functional + shell: bash + run: | + # Create a simple Go program and run it + mkdir -p /tmp/test-go && cd /tmp/test-go + cat > main.go << 'EOF' + package main + import "fmt" + func main() { + fmt.Println("Hello from Microsoft build of Go!") + } + EOF + go run main.go + + microsoft-env-var: + name: 'Microsoft build of Go via env var on ${{ matrix.os }}' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + env: + GO_DOWNLOAD_BASE_URL: 'https://aka.ms/golang/release/latest' + steps: + - uses: actions/checkout@v6 + + - name: Setup Microsoft build of Go via environment variable + uses: ./ + with: + go-version: '1.25' + cache: false + + - name: Verify Go installation + run: go version + + - name: Verify Go is functional + shell: bash + run: | + mkdir -p /tmp/test-go && cd /tmp/test-go + cat > main.go << 'EOF' + package main + import "fmt" + func main() { + fmt.Println("Hello from Microsoft build of Go via env var!") + } + EOF + go run main.go + + microsoft-architecture: + name: 'Microsoft build of Go arch ${{ matrix.architecture }} on ${{ matrix.os }}' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + architecture: [x64] + include: + - os: macos-latest + architecture: arm64 + steps: + - uses: actions/checkout@v6 + + - name: Setup Microsoft build of Go with architecture + uses: ./ + with: + go-version: '1.25' + go-download-base-url: 'https://aka.ms/golang/release/latest' + architecture: ${{ matrix.architecture }} + cache: false + + - name: Verify Go installation + run: go version + + microsoft-with-cache: + name: 'Microsoft build of Go with caching on ${{ matrix.os }}' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v6 + + - name: Setup Microsoft build of Go with caching + uses: ./ + with: + go-version: '1.25' + go-download-base-url: 'https://aka.ms/golang/release/latest' + cache: true + + - name: Verify Go installation + run: go version + + - name: Verify Go is functional + shell: bash + run: | + mkdir -p /tmp/test-go && cd /tmp/test-go + go mod init test + cat > main.go << 'EOF' + package main + import "fmt" + func main() { + fmt.Println("Hello from cached Microsoft build of Go!") + } + EOF + go run main.go diff --git a/README.md b/README.md index 1eec49d..32c86d7 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,9 @@ See [action.yml](action.yml). # Architecture to install (auto-detected if not specified) architecture: 'x64' + + # Custom base URL for Go downloads (e.g., for mirrors) + go-download-base-url: '' ``` @@ -130,6 +133,7 @@ For examples of using `cache-dependency-path`, see the [Caching](docs/advanced-u - [Check latest version](docs/advanced-usage.md#check-latest-version) - [Caching](docs/advanced-usage.md#caching) - [Outputs](docs/advanced-usage.md#outputs) +- [Custom download URL](docs/advanced-usage.md#custom-download-url) - [Using `setup-go` on GHES](docs/advanced-usage.md#using-setup-go-on-ghes) ## License diff --git a/__tests__/setup-go.test.ts b/__tests__/setup-go.test.ts index 8cefcce..5015e91 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'); @@ -129,8 +134,9 @@ describe('setup-go', () => { }); afterEach(() => { - // clear out env var set during 'run' + // clear out env vars set during 'run' delete process.env[im.GOTOOLCHAIN_ENV_VAR]; + delete process.env['GO_DOWNLOAD_BASE_URL']; //jest.resetAllMocks(); jest.clearAllMocks(); @@ -1105,4 +1111,456 @@ use . expect(vars).toStrictEqual({GOTOOLCHAIN: 'local'}); expect(process.env).toHaveProperty('GOTOOLCHAIN', 'local'); }); + + describe('go-download-base-url', () => { + it('downloads a version from custom base URL using version listing', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.13.1'; + const customBaseUrl = 'https://example.com/golang'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + const toolPath = path.normalize('/cache/go/1.13.1/x64'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + const expPath = path.join(toolPath, 'bin'); + expect(logSpy).toHaveBeenCalledWith( + `Using custom Go download base URL: ${customBaseUrl}` + ); + expect(logSpy).toHaveBeenCalledWith('Install from custom download URL'); + // Version listing should use custom base URL, not go.dev + expect(getSpy).toHaveBeenCalledWith( + `${customBaseUrl}/?mode=json&include=all` + ); + expect(dlSpy).toHaveBeenCalled(); + expect(extractTarSpy).toHaveBeenCalled(); + expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`); + }); + + it('skips version listing for known direct-download URL (aka.ms)', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.25.0'; + const customBaseUrl = 'https://aka.ms/golang/release/latest'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + const toolPath = path.normalize('/cache/go/1.25.0/x64'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + const expPath = path.join(toolPath, 'bin'); + expect(logSpy).toHaveBeenCalledWith( + 'Skipping version listing for known direct-download URL. Constructing download URL directly.' + ); + expect(logSpy).toHaveBeenCalledWith( + `Constructed direct download URL: ${customBaseUrl}/go1.25.0.linux-amd64.tar.gz` + ); + expect(logSpy).toHaveBeenCalledWith('Install from custom download URL'); + expect(getSpy).not.toHaveBeenCalled(); + expect(dlSpy).toHaveBeenCalled(); + expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`); + }); + + it('constructs correct direct download URL for windows (aka.ms)', async () => { + os.platform = 'win32'; + os.arch = 'x64'; + + const versionSpec = '1.25.0'; + const customBaseUrl = 'https://aka.ms/golang/release/latest'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + process.env['RUNNER_TEMP'] = 'C:\\temp\\'; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => 'C:\\temp\\some\\path'); + extractZipSpy.mockImplementation(() => 'C:\\temp\\some\\other\\path'); + const toolPath = path.normalize('C:\\cache\\go\\1.25.0\\x64'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + expect(getSpy).not.toHaveBeenCalled(); + expect(dlSpy).toHaveBeenCalledWith( + `${customBaseUrl}/go1.25.0.windows-amd64.zip`, + 'C:\\temp\\go1.25.0.windows-amd64.zip', + undefined + ); + }); + + it('skips manifest and downloads directly from custom URL', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.12.16'; + const customBaseUrl = 'https://example.com/golang'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + inputs['token'] = 'faketoken'; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + const toolPath = path.normalize('/cache/go/1.12.16/x64'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + // Should not try to use the manifest at all + expect(logSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Not found in manifest') + ); + expect(logSpy).toHaveBeenCalledWith('Install from custom download URL'); + }); + + it('strips trailing slashes from custom base URL', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.13.1'; + const customBaseUrl = 'https://example.com/golang/'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + const toolPath = path.normalize('/cache/go/1.13.1/x64'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith( + `Acquiring go1.13.1 from https://example.com/golang/go1.13.1.linux-amd64.tar.gz` + ); + }); + + it('reads custom base URL from environment variable', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.13.1'; + const customBaseUrl = 'https://example.com/golang'; + + inputs['go-version'] = versionSpec; + process.env['GO_DOWNLOAD_BASE_URL'] = customBaseUrl; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + const toolPath = path.normalize('/cache/go/1.13.1/x64'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith( + `Using custom Go download base URL: ${customBaseUrl}` + ); + expect(logSpy).toHaveBeenCalledWith('Install from custom download URL'); + }); + + it('input takes precedence over environment variable', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.13.1'; + const inputUrl = 'https://input.example.com/golang'; + const envUrl = 'https://env.example.com/golang'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = inputUrl; + process.env['GO_DOWNLOAD_BASE_URL'] = envUrl; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + const toolPath = path.normalize('/cache/go/1.13.1/x64'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith( + `Using custom Go download base URL: ${inputUrl}` + ); + expect(logSpy).toHaveBeenCalledWith( + `Acquiring go1.13.1 from ${inputUrl}/go1.13.1.linux-amd64.tar.gz` + ); + }); + + it('errors when stable alias is used with custom URL', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + inputs['go-version'] = 'stable'; + inputs['go-download-base-url'] = 'https://example.com/golang'; + + findSpy.mockImplementation(() => ''); + await main.run(); + + expect(cnSpy).toHaveBeenCalledWith( + `::error::Version aliases 'stable' are not supported with a custom download base URL. Please specify an exact Go version.${osm.EOL}` + ); + }); + + it('logs info when check-latest is used with custom URL', async () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const versionSpec = '1.13.1'; + const customBaseUrl = 'https://example.com/golang'; + + inputs['go-version'] = versionSpec; + inputs['go-download-base-url'] = customBaseUrl; + inputs['check-latest'] = true; + + findSpy.mockImplementation(() => ''); + dlSpy.mockImplementation(async () => '/some/temp/path'); + const toolPath = path.normalize('/cache/go/1.13.1/x64'); + extractTarSpy.mockImplementation(async () => '/some/other/temp/path'); + cacheSpy.mockImplementation(async () => toolPath); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith( + 'check-latest is not supported with a custom download base URL. Using the provided version spec directly.' + ); + }); + + it('constructs direct download info correctly', () => { + os.platform = 'linux'; + os.arch = 'x64'; + + const info = im.getInfoFromDirectDownload( + '1.25.0', + 'x64', + 'https://aka.ms/golang/release/latest' + ); + + expect(info.type).toBe('dist'); + expect(info.downloadUrl).toBe( + 'https://aka.ms/golang/release/latest/go1.25.0.linux-amd64.tar.gz' + ); + expect(info.fileName).toBe('go1.25.0.linux-amd64.tar.gz'); + expect(info.resolvedVersion).toBe('1.25.0'); + }); + + it('constructs direct download info for windows', () => { + os.platform = 'win32'; + os.arch = 'x64'; + + const info = im.getInfoFromDirectDownload( + '1.25.0', + 'x64', + 'https://aka.ms/golang/release/latest' + ); + + expect(info.type).toBe('dist'); + expect(info.downloadUrl).toBe( + 'https://aka.ms/golang/release/latest/go1.25.0.windows-amd64.zip' + ); + expect(info.fileName).toBe('go1.25.0.windows-amd64.zip'); + }); + + it('constructs direct download info for arm64', () => { + os.platform = 'darwin'; + os.arch = 'arm64'; + + const info = im.getInfoFromDirectDownload( + '1.25.0', + 'arm64', + 'https://aka.ms/golang/release/latest' + ); + + expect(info.type).toBe('dist'); + expect(info.downloadUrl).toBe( + 'https://aka.ms/golang/release/latest/go1.25.0.darwin-arm64.tar.gz' + ); + 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; + + 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 expectedToolName = im.customToolCacheName(customBaseUrl); + const toolPath = path.normalize(`/cache/${expectedToolName}/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), + expectedToolName, + '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 expectedToolName = im.customToolCacheName(customBaseUrl); + const toolPath = path.normalize(`/cache/${expectedToolName}/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/action.yml b/action.yml index c5726e1..74a4e7e 100644 --- a/action.yml +++ b/action.yml @@ -19,6 +19,8 @@ inputs: description: 'Used to specify the path to a dependency file (e.g., go.mod, go.sum)' architecture: description: 'Target architecture for Go to use. Examples: x86, x64. Will use system architecture by default.' + go-download-base-url: + description: 'Custom base URL for downloading Go distributions. Use this to download Go from a mirror or custom source. Defaults to "https://go.dev/dl". Can also be set via the GO_DOWNLOAD_BASE_URL environment variable. The input takes precedence over the environment variable.' outputs: go-version: description: 'The installed Go version. Useful when given a version range as input.' diff --git a/dist/setup/index.js b/dist/setup/index.js index 069b6a5..796803e 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -77034,9 +77034,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.GOTOOLCHAIN_LOCAL_VAL = exports.GOTOOLCHAIN_ENV_VAR = void 0; exports.getGo = getGo; +exports.customToolCacheName = customToolCacheName; exports.extractGoArchive = extractGoArchive; exports.getManifest = getManifest; exports.getInfoFromManifest = getInfoFromManifest; +exports.getInfoFromDirectDownload = getInfoFromDirectDownload; exports.findMatch = findMatch; exports.getVersionsDist = getVersionsDist; exports.makeSemver = makeSemver; @@ -77048,6 +77050,8 @@ 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 crypto_1 = __importDefault(__nccwpck_require__(76982)); +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); @@ -77057,14 +77061,23 @@ const MANIFEST_REPO_OWNER = 'actions'; const MANIFEST_REPO_NAME = 'go-versions'; const MANIFEST_REPO_BRANCH = 'main'; const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`; +const DEFAULT_GO_DOWNLOAD_BASE_URL = 'https://go.dev/dl'; const GOLANG_DOWNLOAD_URL = 'https://go.dev/dl/?mode=json&include=all'; +// Base URLs known to not serve a version listing JSON endpoint. +// For these URLs we skip the getInfoFromDist() call entirely and construct +// the download URL directly, avoiding a guaranteed-404 HTTP request. +const NO_VERSION_LISTING_BASE_URLS = ['https://aka.ms/golang/release/latest']; function getGo(versionSpec_1, checkLatest_1, auth_1) { - return __awaiter(this, arguments, void 0, function* (versionSpec, checkLatest, auth, arch = os_1.default.arch()) { + return __awaiter(this, arguments, void 0, function* (versionSpec, checkLatest, auth, arch = os_1.default.arch(), goDownloadBaseUrl) { var _a; let manifest; const osPlat = os_1.default.platform(); + const customBaseUrl = goDownloadBaseUrl === null || goDownloadBaseUrl === void 0 ? void 0 : goDownloadBaseUrl.replace(/\/+$/, ''); if (versionSpec === utils_1.StableReleaseAlias.Stable || versionSpec === utils_1.StableReleaseAlias.OldStable) { + if (customBaseUrl) { + throw new Error(`Version aliases '${versionSpec}' are not supported with a custom download base URL. Please specify an exact Go version.`); + } manifest = yield getManifest(auth); let stableVersion = yield resolveStableVersionInput(versionSpec, arch, osPlat, manifest); if (!stableVersion) { @@ -77077,18 +77090,28 @@ function getGo(versionSpec_1, checkLatest_1, auth_1) { versionSpec = stableVersion; } if (checkLatest) { - core.info('Attempting to resolve the latest version from the manifest...'); - const resolvedVersion = yield resolveVersionFromManifest(versionSpec, true, auth, arch, manifest); - if (resolvedVersion) { - versionSpec = resolvedVersion; - core.info(`Resolved as '${versionSpec}'`); + if (customBaseUrl) { + core.info('check-latest is not supported with a custom download base URL. Using the provided version spec directly.'); } else { - core.info(`Failed to resolve version ${versionSpec} from manifest`); + core.info('Attempting to resolve the latest version from the manifest...'); + const resolvedVersion = yield resolveVersionFromManifest(versionSpec, true, auth, arch, manifest); + if (resolvedVersion) { + versionSpec = resolvedVersion; + core.info(`Resolved as '${versionSpec}'`); + } + else { + core.info(`Failed to resolve version ${versionSpec} from manifest`); + } } } + // Use a distinct tool cache name for custom downloads to avoid + // colliding with the runner's pre-installed Go + const toolCacheName = customBaseUrl + ? customToolCacheName(customBaseUrl) + : 'go'; // check cache - const toolPath = tc.find('go', versionSpec, arch); + const toolPath = tc.find(toolCacheName, versionSpec, arch); // If not found in cache, download if (toolPath) { core.info(`Found in cache @ ${toolPath}`); @@ -77097,43 +77120,79 @@ function getGo(versionSpec_1, checkLatest_1, auth_1) { core.info(`Attempting to download ${versionSpec}...`); let downloadPath = ''; let info = null; - // - // Try download from internal distribution (popular versions only) - // - try { - info = yield getInfoFromManifest(versionSpec, true, auth, arch, manifest); - if (info) { - downloadPath = yield installGoVersion(info, auth, arch); + if (customBaseUrl) { + // + // Download from custom base URL + // + 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); } else { - core.info('Not found in manifest. Falling back to download directly from Go'); - } - } - catch (err) { - if (err instanceof tc.HTTPError && - (err.httpStatusCode === 403 || err.httpStatusCode === 429)) { - core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); - } - else { - core.info(err.message); - } - core.debug((_a = err.stack) !== null && _a !== void 0 ? _a : ''); - core.info('Falling back to download directly from Go'); - } - // - // Download from storage.googleapis.com - // - if (!downloadPath) { - info = yield getInfoFromDist(versionSpec, arch); - if (!info) { - throw new Error(`Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.`); + try { + info = yield getInfoFromDist(versionSpec, arch, customBaseUrl); + } + catch (_b) { + core.info('Version listing not available from custom URL. Constructing download URL directly.'); + } + if (!info) { + info = getInfoFromDirectDownload(versionSpec, arch, customBaseUrl); + } } try { - core.info('Install from dist'); - downloadPath = yield installGoVersion(info, undefined, arch); + core.info('Install from custom download URL'); + 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 { + // + // Try download from internal distribution (popular versions only) + // + try { + info = yield getInfoFromManifest(versionSpec, true, auth, arch, manifest); + if (info) { + downloadPath = yield installGoVersion(info, auth, arch); + } + else { + core.info('Not found in manifest. Falling back to download directly from Go'); + } + } + catch (err) { + if (err instanceof tc.HTTPError && + (err.httpStatusCode === 403 || err.httpStatusCode === 429)) { + core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); + } + else { + core.info(err.message); + } + core.debug((_a = err.stack) !== null && _a !== void 0 ? _a : ''); + core.info('Falling back to download directly from Go'); + } + // + // Download from storage.googleapis.com + // + if (!downloadPath) { + info = yield getInfoFromDist(versionSpec, arch); + if (!info) { + throw new Error(`Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.`); + } + try { + core.info('Install from dist'); + downloadPath = yield installGoVersion(info, undefined, arch); + } + catch (err) { + throw new Error(`Failed to download version ${versionSpec}: ${err}`); + } } } return downloadPath; @@ -77186,16 +77245,19 @@ function cacheWindowsDir(extPath, tool, version, arch) { return defaultToolCacheDir; }); } -function addExecutablesToToolCache(extPath, info, arch) { - return __awaiter(this, void 0, void 0, function* () { - const tool = 'go'; +function addExecutablesToToolCache(extPath_1, info_1, arch_1) { + return __awaiter(this, arguments, void 0, function* (extPath, info, arch, toolName = 'go') { const version = makeSemver(info.resolvedVersion); - return ((yield cacheWindowsDir(extPath, tool, version, arch)) || - (yield tc.cacheDir(extPath, tool, version, arch))); + return ((yield cacheWindowsDir(extPath, toolName, version, arch)) || + (yield tc.cacheDir(extPath, toolName, version, arch))); }); } -function installGoVersion(info, auth, arch) { - return __awaiter(this, void 0, void 0, function* () { +function customToolCacheName(baseUrl) { + const hash = crypto_1.default.createHash('sha256').update(baseUrl).digest('hex'); + return `go-${hash.substring(0, 8)}`; +} +function installGoVersion(info_1, auth_1, arch_1) { + return __awaiter(this, arguments, void 0, function* (info, auth, arch, toolName = 'go') { core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`); // Windows requires that we keep the extension (.zip) for extraction const isWindows = os_1.default.platform() === 'win32'; @@ -77208,12 +77270,33 @@ function installGoVersion(info, auth, arch) { 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); + 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(); @@ -77298,13 +77381,17 @@ function getInfoFromManifest(versionSpec_1, stable_1, auth_1) { return info; }); } -function getInfoFromDist(versionSpec, arch) { +function getInfoFromDist(versionSpec, arch, goDownloadBaseUrl) { return __awaiter(this, void 0, void 0, function* () { - const version = yield findMatch(versionSpec, arch); + const dlUrl = goDownloadBaseUrl + ? `${goDownloadBaseUrl}/?mode=json&include=all` + : GOLANG_DOWNLOAD_URL; + const version = yield findMatch(versionSpec, arch, dlUrl); if (!version) { return null; } - const downloadUrl = `https://go.dev/dl/${version.files[0].filename}`; + const baseUrl = goDownloadBaseUrl || DEFAULT_GO_DOWNLOAD_BASE_URL; + const downloadUrl = `${baseUrl}/${version.files[0].filename}`; return { type: 'dist', downloadUrl: downloadUrl, @@ -77313,13 +77400,36 @@ function getInfoFromDist(versionSpec, arch) { }; }); } +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'; + // Ensure version has the 'go' prefix for the filename + const goVersion = versionSpec.startsWith('go') + ? versionSpec + : `go${versionSpec}`; + const fileName = `${goVersion}.${platStr}-${archStr}.${extension}`; + const downloadUrl = `${goDownloadBaseUrl}/${fileName}`; + core.info(`Constructed direct download URL: ${downloadUrl}`); + return { + type: 'dist', + downloadUrl: downloadUrl, + resolvedVersion: versionSpec.replace(/^go/, ''), + fileName: fileName + }; +} function findMatch(versionSpec_1) { - return __awaiter(this, arguments, void 0, function* (versionSpec, arch = os_1.default.arch()) { + return __awaiter(this, arguments, void 0, function* (versionSpec, arch = os_1.default.arch(), dlUrl = GOLANG_DOWNLOAD_URL) { const archFilter = sys.getArch(arch); const platFilter = sys.getPlatform(); let result; let match; - const candidates = yield module.exports.getVersionsDist(GOLANG_DOWNLOAD_URL); + const candidates = yield module.exports.getVersionsDist(dlUrl); if (!candidates) { throw new Error(`golang download url did not return results`); } @@ -77529,7 +77639,13 @@ function run() { const token = core.getInput('token'); const auth = !token ? undefined : `token ${token}`; const checkLatest = core.getBooleanInput('check-latest'); - const installDir = yield installer.getGo(versionSpec, checkLatest, auth, arch); + const goDownloadBaseUrl = core.getInput('go-download-base-url') || + process.env['GO_DOWNLOAD_BASE_URL'] || + undefined; + if (goDownloadBaseUrl) { + core.info(`Using custom Go download base URL: ${goDownloadBaseUrl}`); + } + const installDir = yield installer.getGo(versionSpec, checkLatest, auth, arch, goDownloadBaseUrl); const installDirVersion = path_1.default.basename(path_1.default.dirname(installDir)); core.addPath(path_1.default.join(installDir, 'bin')); core.info('Added go to the path'); diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index ad45103..1ca3810 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -12,6 +12,7 @@ - [Restore-only caches](advanced-usage.md#restore-only-caches) - [Parallel builds](advanced-usage.md#parallel-builds) - [Outputs](advanced-usage.md#outputs) +- [Custom download URL](advanced-usage.md#custom-download-url) - [Using `setup-go` on GHES](advanced-usage.md#using-setup-go-on-ghes) ## Using the `go-version` input @@ -222,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: @@ -417,6 +420,57 @@ jobs: - run: echo "Was the Go cache restored? ${{ steps.go124.outputs.cache-hit }}" # true if cache-hit occurred ``` +## Custom download URL + +The `go-download-base-url` input lets you download Go from a mirror or alternative source instead of the default `https://go.dev/dl`. This can also be set via the `GO_DOWNLOAD_BASE_URL` environment variable; the input takes precedence over the environment variable. + +When a custom base URL is provided, the action skips the `actions/go-versions` manifest lookup and downloads directly from the specified URL. + +**Using the [Microsoft build of Go](https://github.com/nicholasgasior/microsoft-go):** + +```yaml +steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: '1.25' + go-download-base-url: 'https://aka.ms/golang/release/latest' + - run: go version +``` + +**Using an environment variable:** + +```yaml +env: + GO_DOWNLOAD_BASE_URL: 'https://aka.ms/golang/release/latest' + +steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: '1.25' + - run: go version +``` + +> **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 ### Avoiding rate limit issues diff --git a/src/installer.ts b/src/installer.ts index 15e665d..e845e31 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -4,6 +4,8 @@ import * as path from 'path'; import * as semver from 'semver'; import * as httpm from '@actions/http-client'; import * as sys from './system'; +import crypto from 'crypto'; +import cp from 'child_process'; import fs from 'fs'; import os from 'os'; import {StableReleaseAlias, isSelfHosted} from './utils'; @@ -15,11 +17,17 @@ const MANIFEST_REPO_OWNER = 'actions'; const MANIFEST_REPO_NAME = 'go-versions'; const MANIFEST_REPO_BRANCH = 'main'; const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`; +const DEFAULT_GO_DOWNLOAD_BASE_URL = 'https://go.dev/dl'; type InstallationType = 'dist' | 'manifest'; const GOLANG_DOWNLOAD_URL = 'https://go.dev/dl/?mode=json&include=all'; +// Base URLs known to not serve a version listing JSON endpoint. +// For these URLs we skip the getInfoFromDist() call entirely and construct +// the download URL directly, avoiding a guaranteed-404 HTTP request. +const NO_VERSION_LISTING_BASE_URLS = ['https://aka.ms/golang/release/latest']; + export interface IGoVersionFile { filename: string; // darwin, linux, windows @@ -44,15 +52,23 @@ export async function getGo( versionSpec: string, checkLatest: boolean, auth: string | undefined, - arch: Architecture = os.arch() as Architecture + arch: Architecture = os.arch() as Architecture, + goDownloadBaseUrl?: string ) { let manifest: tc.IToolRelease[] | undefined; const osPlat: string = os.platform(); + const customBaseUrl = goDownloadBaseUrl?.replace(/\/+$/, ''); if ( versionSpec === StableReleaseAlias.Stable || versionSpec === StableReleaseAlias.OldStable ) { + if (customBaseUrl) { + throw new Error( + `Version aliases '${versionSpec}' are not supported with a custom download base URL. Please specify an exact Go version.` + ); + } + manifest = await getManifest(auth); let stableVersion = await resolveStableVersionInput( versionSpec, @@ -76,24 +92,38 @@ export async function getGo( } if (checkLatest) { - core.info('Attempting to resolve the latest version from the manifest...'); - const resolvedVersion = await resolveVersionFromManifest( - versionSpec, - true, - auth, - arch, - manifest - ); - if (resolvedVersion) { - versionSpec = resolvedVersion; - core.info(`Resolved as '${versionSpec}'`); + if (customBaseUrl) { + core.info( + 'check-latest is not supported with a custom download base URL. Using the provided version spec directly.' + ); } else { - core.info(`Failed to resolve version ${versionSpec} from manifest`); + core.info( + 'Attempting to resolve the latest version from the manifest...' + ); + const resolvedVersion = await resolveVersionFromManifest( + versionSpec, + true, + auth, + arch, + manifest + ); + if (resolvedVersion) { + versionSpec = resolvedVersion; + core.info(`Resolved as '${versionSpec}'`); + } else { + core.info(`Failed to resolve version ${versionSpec} from manifest`); + } } } + // Use a distinct tool cache name for custom downloads to avoid + // colliding with the runner's pre-installed Go + const toolCacheName = customBaseUrl + ? customToolCacheName(customBaseUrl) + : 'go'; + // check cache - const toolPath = tc.find('go', versionSpec, arch); + const toolPath = tc.find(toolCacheName, versionSpec, arch); // If not found in cache, download if (toolPath) { core.info(`Found in cache @ ${toolPath}`); @@ -103,49 +133,93 @@ export async function getGo( let downloadPath = ''; let info: IGoVersionInfo | null = null; - // - // Try download from internal distribution (popular versions only) - // - try { - info = await getInfoFromManifest(versionSpec, true, auth, arch, manifest); - if (info) { - downloadPath = await installGoVersion(info, auth, arch); - } else { - core.info( - 'Not found in manifest. Falling back to download directly from Go' - ); - } - } catch (err) { - if ( - err instanceof tc.HTTPError && - (err.httpStatusCode === 403 || err.httpStatusCode === 429) - ) { - core.info( - `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` - ); - } else { - core.info((err as Error).message); - } - core.debug((err as Error).stack ?? ''); - core.info('Falling back to download directly from Go'); - } + if (customBaseUrl) { + // + // Download from custom base URL + // + const skipVersionListing = NO_VERSION_LISTING_BASE_URLS.some( + url => customBaseUrl.toLowerCase() === url.toLowerCase() + ); - // - // Download from storage.googleapis.com - // - if (!downloadPath) { - info = await getInfoFromDist(versionSpec, arch); - if (!info) { - throw new Error( - `Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.` + if (skipVersionListing) { + core.info( + 'Skipping version listing for known direct-download URL. Constructing download URL directly.' ); + info = getInfoFromDirectDownload(versionSpec, arch, customBaseUrl); + } else { + try { + info = await getInfoFromDist(versionSpec, arch, customBaseUrl); + } catch { + core.info( + 'Version listing not available from custom URL. Constructing download URL directly.' + ); + } + if (!info) { + info = getInfoFromDirectDownload(versionSpec, arch, customBaseUrl); + } } try { - core.info('Install from dist'); - downloadPath = await installGoVersion(info, undefined, arch); + core.info('Install from custom download URL'); + downloadPath = await installGoVersion(info, 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 { + // + // Try download from internal distribution (popular versions only) + // + try { + info = await getInfoFromManifest(versionSpec, true, auth, arch, manifest); + if (info) { + downloadPath = await installGoVersion(info, auth, arch); + } else { + core.info( + 'Not found in manifest. Falling back to download directly from Go' + ); + } + } catch (err) { + if ( + err instanceof tc.HTTPError && + (err.httpStatusCode === 403 || err.httpStatusCode === 429) + ) { + core.info( + `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` + ); + } else { + core.info((err as Error).message); + } + core.debug((err as Error).stack ?? ''); + core.info('Falling back to download directly from Go'); + } + + // + // Download from storage.googleapis.com + // + if (!downloadPath) { + info = await getInfoFromDist(versionSpec, arch); + if (!info) { + throw new Error( + `Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.` + ); + } + + try { + core.info('Install from dist'); + downloadPath = await installGoVersion(info, undefined, arch); + } catch (err) { + throw new Error(`Failed to download version ${versionSpec}: ${err}`); + } } } @@ -229,20 +303,26 @@ async function cacheWindowsDir( async function addExecutablesToToolCache( extPath: string, info: IGoVersionInfo, - arch: string + arch: string, + toolName: string = 'go' ): Promise { - const tool = 'go'; const version = makeSemver(info.resolvedVersion); return ( - (await cacheWindowsDir(extPath, tool, version, arch)) || - (await tc.cacheDir(extPath, tool, version, arch)) + (await cacheWindowsDir(extPath, toolName, version, arch)) || + (await tc.cacheDir(extPath, toolName, version, arch)) ); } +export function customToolCacheName(baseUrl: string): string { + const hash = crypto.createHash('sha256').update(baseUrl).digest('hex'); + return `go-${hash.substring(0, 8)}`; +} + async function installGoVersion( info: IGoVersionInfo, auth: string | undefined, - arch: string + arch: string, + toolName: string = 'go' ): Promise { core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`); @@ -260,13 +340,48 @@ 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, info, arch); + const toolCacheDir = await addExecutablesToToolCache( + extPath, + info, + arch, + toolName + ); core.info(`Successfully cached go to ${toolCacheDir}`); 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; @@ -384,14 +499,23 @@ export async function getInfoFromManifest( async function getInfoFromDist( versionSpec: string, - arch: Architecture + arch: Architecture, + goDownloadBaseUrl?: string ): Promise { - const version: IGoVersion | undefined = await findMatch(versionSpec, arch); + const dlUrl = goDownloadBaseUrl + ? `${goDownloadBaseUrl}/?mode=json&include=all` + : GOLANG_DOWNLOAD_URL; + const version: IGoVersion | undefined = await findMatch( + versionSpec, + arch, + dlUrl + ); if (!version) { return null; } - const downloadUrl = `https://go.dev/dl/${version.files[0].filename}`; + const baseUrl = goDownloadBaseUrl || DEFAULT_GO_DOWNLOAD_BASE_URL; + const downloadUrl = `${baseUrl}/${version.files[0].filename}`; return { type: 'dist', @@ -401,9 +525,43 @@ async function getInfoFromDist( }; } +export function getInfoFromDirectDownload( + versionSpec: string, + 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'; + // Ensure version has the 'go' prefix for the filename + const goVersion = versionSpec.startsWith('go') + ? versionSpec + : `go${versionSpec}`; + const fileName = `${goVersion}.${platStr}-${archStr}.${extension}`; + const downloadUrl = `${goDownloadBaseUrl}/${fileName}`; + + core.info(`Constructed direct download URL: ${downloadUrl}`); + + return { + type: 'dist', + downloadUrl: downloadUrl, + resolvedVersion: versionSpec.replace(/^go/, ''), + fileName: fileName + }; +} + export async function findMatch( versionSpec: string, - arch: Architecture = os.arch() as Architecture + arch: Architecture = os.arch() as Architecture, + dlUrl: string = GOLANG_DOWNLOAD_URL ): Promise { const archFilter = sys.getArch(arch); const platFilter = sys.getPlatform(); @@ -412,7 +570,7 @@ export async function findMatch( let match: IGoVersion | undefined; const candidates: IGoVersion[] | null = await module.exports.getVersionsDist( - GOLANG_DOWNLOAD_URL + dlUrl ); if (!candidates) { throw new Error(`golang download url did not return results`); diff --git a/src/main.ts b/src/main.ts index 26939ee..e578ed9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,11 +34,21 @@ export async function run() { const checkLatest = core.getBooleanInput('check-latest'); + const goDownloadBaseUrl = + core.getInput('go-download-base-url') || + process.env['GO_DOWNLOAD_BASE_URL'] || + undefined; + + if (goDownloadBaseUrl) { + core.info(`Using custom Go download base URL: ${goDownloadBaseUrl}`); + } + const installDir = await installer.getGo( versionSpec, checkLatest, auth, - arch + arch, + goDownloadBaseUrl ); const installDirVersion = path.basename(path.dirname(installDir));