1
0
mirror of https://github.com/actions/setup-go.git synced 2026-07-16 18:18:05 +00:00
Files
setup-go/src/installer.ts
T
Priya Gupta 0778a10ce4 Migrate to ESM and upgrade dependencies (#763)
* Migrate to ESM and upgrade dependencies

* Remove unused tsconfig.eslint.json

* bump version to 7.0.0 in package.json and package-lock.json

* Add ESM migration note to README for V7

* Remove unnecessary devDependencies: ts-node, @types/jest

* npm audit fix

* Update README with ESM migration details

Clarified migration details to ESM for action compatibility.

* Downgrade @types/node to ^24, clean up tsconfig and README.
2026-07-15 15:34:36 -05:00

737 lines
21 KiB
TypeScript

import * as tc from '@actions/tool-cache';
import * as core from '@actions/core';
import * as path from 'path';
import * as semver from 'semver';
import * as httpm from '@actions/http-client';
import * as sys from './system.js';
import crypto from 'crypto';
import cp from 'child_process';
import fs from 'fs';
import os from 'os';
import {StableReleaseAlias, isSelfHosted} from './utils.js';
import {Architecture} from './types.js';
import {getVersionsDist} from './go-version-fetch.js';
export const GOTOOLCHAIN_ENV_VAR = 'GOTOOLCHAIN';
export const GOTOOLCHAIN_LOCAL_VAL = 'local';
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
os: string;
arch: string;
}
export interface IGoVersion {
version: string;
stable: boolean;
files: IGoVersionFile[];
}
export interface IGoVersionInfo {
type: InstallationType;
downloadUrl: string;
resolvedVersion: string;
fileName: string;
}
export async function getGo(
versionSpec: string,
checkLatest: boolean,
auth: string | undefined,
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,
arch,
osPlat,
manifest
);
if (!stableVersion) {
stableVersion = await resolveStableVersionDist(versionSpec, arch);
if (!stableVersion) {
throw new Error(
`Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.`
);
}
}
core.info(`${versionSpec} version resolved as ${stableVersion}`);
versionSpec = stableVersion;
}
if (checkLatest) {
if (customBaseUrl) {
core.info(
'check-latest is not supported with a custom download base URL. Using the provided version spec directly.'
);
} else {
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(toolCacheName, versionSpec, arch);
// If not found in cache, download
if (toolPath) {
core.info(`Found in cache @ ${toolPath}`);
return toolPath;
}
core.info(`Attempting to download ${versionSpec}...`);
let downloadPath = '';
let info: IGoVersionInfo | null = null;
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 {
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 custom download URL');
downloadPath = await installGoVersion(info, auth, arch, toolCacheName);
} catch (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}`,
{cause: err}
);
}
throw new Error(
`Failed to download Go ${versionSpec} for platform ${osPlat}/${arch} ` +
`from ${downloadUrl}: ${err}`,
{cause: 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}`, {
cause: err
});
}
}
}
return downloadPath;
}
async function resolveVersionFromManifest(
versionSpec: string,
stable: boolean,
auth: string | undefined,
arch: Architecture,
manifest: tc.IToolRelease[] | undefined
): Promise<string | undefined> {
try {
const info = await getInfoFromManifest(
versionSpec,
stable,
auth,
arch,
manifest
);
return info?.resolvedVersion;
} catch (err) {
core.info('Unable to resolve a version from the manifest...');
core.debug((err as Error).message);
}
}
// for github hosted windows runner handle latency of OS drive
// by avoiding write operations to C:
async function cacheWindowsDir(
extPath: string,
tool: string,
version: string,
arch: string
): Promise<string | false> {
if (os.platform() !== 'win32') return false;
// make sure the action runs in the hosted environment
if (isSelfHosted()) return false;
const defaultToolCacheRoot = process.env['RUNNER_TOOL_CACHE'];
if (!defaultToolCacheRoot) return false;
if (!fs.existsSync('d:\\') || !fs.existsSync('c:\\')) return false;
const actualToolCacheRoot = defaultToolCacheRoot
.replace('C:', 'D:')
.replace('c:', 'd:');
// make toolcache root to be on drive d:
process.env['RUNNER_TOOL_CACHE'] = actualToolCacheRoot;
const actualToolCacheDir = await tc.cacheDir(extPath, tool, version, arch);
// create a link from c: to d:
const defaultToolCacheDir = actualToolCacheDir.replace(
actualToolCacheRoot,
defaultToolCacheRoot
);
fs.mkdirSync(path.dirname(defaultToolCacheDir), {recursive: true});
fs.symlinkSync(actualToolCacheDir, defaultToolCacheDir, 'junction');
core.info(`Created link ${defaultToolCacheDir} => ${actualToolCacheDir}`);
const actualToolCacheCompleteFile = `${actualToolCacheDir}.complete`;
const defaultToolCacheCompleteFile = `${defaultToolCacheDir}.complete`;
fs.symlinkSync(
actualToolCacheCompleteFile,
defaultToolCacheCompleteFile,
'file'
);
core.info(
`Created link ${defaultToolCacheCompleteFile} => ${actualToolCacheCompleteFile}`
);
// make outer code to continue using toolcache as if it were installed on c:
// restore toolcache root to default drive c:
process.env['RUNNER_TOOL_CACHE'] = defaultToolCacheRoot;
return defaultToolCacheDir;
}
async function addExecutablesToToolCache(
extPath: string,
info: IGoVersionInfo,
arch: string,
toolName: string = 'go'
): Promise<string> {
const version = makeSemver(info.resolvedVersion);
return (
(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,
toolName: string = 'go'
): Promise<string> {
core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`);
// Windows requires that we keep the extension (.zip) for extraction
const isWindows = os.platform() === 'win32';
const tempDir = process.env.RUNNER_TEMP || '.';
const fileName = isWindows ? path.join(tempDir, info.fileName) : undefined;
const downloadPath = await tc.downloadTool(info.downloadUrl, fileName, auth);
core.info('Extracting Go...');
let extPath = await extractGoArchive(downloadPath);
core.info(`Successfully extracted go to ${extPath}`);
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 = 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<string> {
const platform = os.platform();
let extPath: string;
if (platform === 'win32') {
extPath = await tc.extractZip(archivePath);
} else {
extPath = await tc.extractTar(archivePath);
}
return extPath;
}
function isIToolRelease(obj: any): obj is tc.IToolRelease {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.version === 'string' &&
typeof obj.stable === 'boolean' &&
Array.isArray(obj.files) &&
obj.files.every(
(file: any) =>
typeof file.filename === 'string' &&
typeof file.platform === 'string' &&
typeof file.arch === 'string' &&
typeof file.download_url === 'string'
)
);
}
export async function getManifest(
auth: string | undefined
): Promise<tc.IToolRelease[]> {
try {
const manifest = await getManifestFromRepo(auth);
if (
Array.isArray(manifest) &&
manifest.length &&
manifest.every(isIToolRelease)
) {
return manifest;
}
let errorMessage =
'An unexpected error occurred while fetching the manifest.';
if (
typeof manifest === 'object' &&
manifest !== null &&
'message' in manifest
) {
errorMessage = (manifest as {message: string}).message;
}
throw new Error(errorMessage);
} catch (err) {
core.debug('Fetching the manifest via the API failed.');
if (err instanceof Error) {
core.debug(err.message);
}
}
return await getManifestFromURL();
}
function getManifestFromRepo(
auth: string | undefined
): Promise<tc.IToolRelease[]> {
core.debug(
`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`
);
return tc.getManifestFromRepo(
MANIFEST_REPO_OWNER,
MANIFEST_REPO_NAME,
auth,
MANIFEST_REPO_BRANCH
);
}
async function getManifestFromURL(): Promise<tc.IToolRelease[]> {
core.debug('Falling back to fetching the manifest using raw URL.');
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
const response = await http.getJson<tc.IToolRelease[]>(MANIFEST_URL);
if (!response.result) {
throw new Error(`Unable to get manifest from ${MANIFEST_URL}`);
}
return response.result;
}
export async function getInfoFromManifest(
versionSpec: string,
stable: boolean,
auth: string | undefined,
arch: Architecture = os.arch() as Architecture,
manifest?: tc.IToolRelease[] | undefined
): Promise<IGoVersionInfo | null> {
let info: IGoVersionInfo | null = null;
if (!manifest) {
core.debug('No manifest cached');
manifest = await getManifest(auth);
}
core.info(`matching ${versionSpec}...`);
const rel = await tc.findFromManifest(versionSpec, stable, manifest, arch);
if (rel && rel.files.length > 0) {
info = <IGoVersionInfo>{};
info.type = 'manifest';
info.resolvedVersion = rel.version;
info.downloadUrl = rel.files[0].download_url;
info.fileName = rel.files[0].filename;
}
return info;
}
async function getInfoFromDist(
versionSpec: string,
arch: Architecture,
goDownloadBaseUrl?: string
): Promise<IGoVersionInfo | null> {
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 baseUrl = goDownloadBaseUrl || DEFAULT_GO_DOWNLOAD_BASE_URL;
const downloadUrl = `${baseUrl}/${version.files[0].filename}`;
return <IGoVersionInfo>{
type: 'dist',
downloadUrl: downloadUrl,
resolvedVersion: version.version,
fileName: version.files[0].filename
};
}
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 <IGoVersionInfo>{
type: 'dist',
downloadUrl: downloadUrl,
resolvedVersion: versionSpec.replace(/^go/, ''),
fileName: fileName
};
}
export async function findMatch(
versionSpec: string,
arch: Architecture = os.arch() as Architecture,
dlUrl: string = GOLANG_DOWNLOAD_URL
): Promise<IGoVersion | undefined> {
const archFilter = sys.getArch(arch);
const platFilter = sys.getPlatform();
let result: IGoVersion | undefined;
let match: IGoVersion | undefined;
const candidates: IGoVersion[] | null = await getVersionsDist(dlUrl);
if (!candidates) {
throw new Error(`golang download url did not return results`);
}
let goFile: IGoVersionFile | undefined;
for (let i = 0; i < candidates.length; i++) {
const candidate: IGoVersion = candidates[i];
const version = makeSemver(candidate.version);
core.debug(`check ${version} satisfies ${versionSpec}`);
if (semver.satisfies(version, versionSpec)) {
goFile = candidate.files.find(file => {
core.debug(
`${file.arch}===${archFilter} && ${file.os}===${platFilter}`
);
return file.arch === archFilter && file.os === platFilter;
});
if (goFile) {
core.debug(`matched ${candidate.version}`);
match = candidate;
break;
}
}
}
if (match && goFile) {
// clone since we're mutating the file list to be only the file that matches
result = <IGoVersion>Object.assign({}, match);
result.files = [goFile];
}
return result;
}
//
// Convert the go version syntax into semver for semver matching
// 1.13.1 => 1.13.1
// 1.13 => 1.13.0
// 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 {
version = version.replace('go', '');
version = version.replace('beta', '-beta.').replace('rc', '-rc.');
const parts = version.split('-');
const semVersion = semver.coerce(parts[0])?.version;
if (!semVersion) {
throw new Error(
`The version: ${version} can't be changed to SemVer notation`
);
}
if (!parts[1]) {
return semVersion;
}
const fullVersion = semver.valid(`${semVersion}-${parts[1]}`);
if (!fullVersion) {
throw new Error(
`The version: ${version} can't be changed to SemVer notation`
);
}
return fullVersion;
}
export function parseGoVersionFile(versionFilePath: string): string {
const contents = fs.readFileSync(versionFilePath).toString();
if (
path.basename(versionFilePath) === 'go.mod' ||
path.basename(versionFilePath) === 'go.work'
) {
// for backwards compatibility: use version from go directive if
// 'GOTOOLCHAIN' has been explicitly set
if (process.env[GOTOOLCHAIN_ENV_VAR] !== GOTOOLCHAIN_LOCAL_VAL) {
// toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain
const matchToolchain = contents.match(
/^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m
);
if (matchToolchain) {
return matchToolchain[1];
}
}
// go directive: https://go.dev/ref/mod#go-mod-file-go
const matchGo = contents.match(/^go (\d+(\.\d+)*)/m);
return matchGo ? matchGo[1] : '';
} else if (path.basename(versionFilePath) === '.tool-versions') {
const match = contents.match(/^golang\s+([^\n#]+)/m);
return match ? match[1].trim() : '';
}
return contents.trim();
}
async function resolveStableVersionDist(
versionSpec: string,
arch: Architecture
) {
const archFilter = sys.getArch(arch);
const platFilter = sys.getPlatform();
const candidates: IGoVersion[] | null =
await getVersionsDist(GOLANG_DOWNLOAD_URL);
if (!candidates) {
throw new Error(`golang download url did not return results`);
}
const fixedCandidates = candidates.map(item => {
return {
...item,
version: makeSemver(item.version)
};
});
const stableVersion = await resolveStableVersionInput(
versionSpec,
archFilter,
platFilter,
fixedCandidates
);
return stableVersion;
}
export async function resolveStableVersionInput(
versionSpec: string,
arch: string,
platform: string,
manifest: tc.IToolRelease[] | IGoVersion[]
) {
const releases = manifest
.map(item => {
const index = item.files.findIndex(
item => item.arch === arch && item.filename.includes(platform)
);
if (index === -1) {
return '';
}
return item.version;
})
.filter(item => !!item && !semver.prerelease(item));
if (versionSpec === StableReleaseAlias.Stable) {
return releases[0];
} else {
const versions = releases.map(
release => `${semver.major(release)}.${semver.minor(release)}`
);
const uniqueVersions = Array.from(new Set(versions));
const oldstableVersion = releases.find(item =>
item.startsWith(uniqueVersions[1])
);
return oldstableVersion;
}
}