actions/setup-go v5 (local mirror)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import * as cache from '@actions/cache';
|
||||
import * as core from '@actions/core';
|
||||
import * as glob from '@actions/glob';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import {State, Outputs} from './constants';
|
||||
import {PackageManagerInfo} from './package-managers';
|
||||
import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils';
|
||||
|
||||
export const restoreCache = async (
|
||||
versionSpec: string,
|
||||
packageManager: string,
|
||||
cacheDependencyPath?: string
|
||||
) => {
|
||||
const packageManagerInfo = await getPackageManagerInfo(packageManager);
|
||||
const platform = process.env.RUNNER_OS;
|
||||
const arch = process.arch;
|
||||
|
||||
const cachePaths = await getCacheDirectoryPath(packageManagerInfo);
|
||||
|
||||
const dependencyFilePath = cacheDependencyPath
|
||||
? cacheDependencyPath
|
||||
: findDependencyFile(packageManagerInfo);
|
||||
const fileHash = await glob.hashFiles(dependencyFilePath);
|
||||
|
||||
if (!fileHash) {
|
||||
throw new Error(
|
||||
'Some specified paths were not resolved, unable to cache dependencies.'
|
||||
);
|
||||
}
|
||||
|
||||
const linuxVersion =
|
||||
process.env.RUNNER_OS === 'Linux' ? `${process.env.ImageOS}-` : '';
|
||||
const primaryKey = `setup-go-${platform}-${arch}-${linuxVersion}go-${versionSpec}-${fileHash}`;
|
||||
core.debug(`primary key is ${primaryKey}`);
|
||||
|
||||
core.saveState(State.CachePrimaryKey, primaryKey);
|
||||
|
||||
const cacheKey = await cache.restoreCache(cachePaths, primaryKey);
|
||||
core.setOutput(Outputs.CacheHit, Boolean(cacheKey));
|
||||
|
||||
if (!cacheKey) {
|
||||
core.info(`Cache is not found`);
|
||||
core.setOutput(Outputs.CacheHit, false);
|
||||
return;
|
||||
}
|
||||
|
||||
core.saveState(State.CacheMatchedKey, cacheKey);
|
||||
core.info(`Cache restored from key: ${cacheKey}`);
|
||||
};
|
||||
|
||||
const findDependencyFile = (packageManager: PackageManagerInfo) => {
|
||||
const dependencyFile = packageManager.dependencyFilePattern;
|
||||
const workspace = process.env.GITHUB_WORKSPACE!;
|
||||
const rootContent = fs.readdirSync(workspace);
|
||||
|
||||
const goSumFileExists = rootContent.includes(dependencyFile);
|
||||
if (!goSumFileExists) {
|
||||
throw new Error(
|
||||
`Dependencies file is not found in ${workspace}. Supported file pattern: ${dependencyFile}`
|
||||
);
|
||||
}
|
||||
|
||||
return path.join(workspace, dependencyFile);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as cache from '@actions/cache';
|
||||
import fs from 'fs';
|
||||
import {State} from './constants';
|
||||
import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils';
|
||||
|
||||
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
||||
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
||||
// throw an uncaught exception. Instead of failing this action, just warn.
|
||||
process.on('uncaughtException', e => {
|
||||
const warningPrefix = '[warning]';
|
||||
core.info(`${warningPrefix}${e.message}`);
|
||||
});
|
||||
|
||||
// Added early exit to resolve issue with slow post action step:
|
||||
// - https://github.com/actions/setup-node/issues/878
|
||||
// https://github.com/actions/cache/pull/1217
|
||||
export async function run(earlyExit?: boolean) {
|
||||
try {
|
||||
const cacheInput = core.getBooleanInput('cache');
|
||||
if (cacheInput) {
|
||||
await cachePackages();
|
||||
|
||||
if (earlyExit) {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
let message = 'Unknown error!';
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
message = error;
|
||||
}
|
||||
core.warning(message);
|
||||
}
|
||||
}
|
||||
|
||||
const cachePackages = async () => {
|
||||
const packageManager = 'default';
|
||||
|
||||
const state = core.getState(State.CacheMatchedKey);
|
||||
const primaryKey = core.getState(State.CachePrimaryKey);
|
||||
|
||||
const packageManagerInfo = await getPackageManagerInfo(packageManager);
|
||||
|
||||
const cachePaths = await getCacheDirectoryPath(packageManagerInfo);
|
||||
|
||||
const nonExistingPaths = cachePaths.filter(
|
||||
cachePath => !fs.existsSync(cachePath)
|
||||
);
|
||||
|
||||
if (nonExistingPaths.length === cachePaths.length) {
|
||||
core.warning('There are no cache folders on the disk');
|
||||
return;
|
||||
}
|
||||
|
||||
if (nonExistingPaths.length) {
|
||||
logWarning(
|
||||
`Cache folder path is retrieved but doesn't exist on disk: ${nonExistingPaths.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!primaryKey) {
|
||||
core.info(
|
||||
'Primary key was not generated. Please check the log messages above for more errors or information'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (primaryKey === state) {
|
||||
core.info(
|
||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheId = await cache.saveCache(cachePaths, primaryKey);
|
||||
if (cacheId === -1) {
|
||||
return;
|
||||
}
|
||||
core.info(`Cache saved with the key: ${primaryKey}`);
|
||||
};
|
||||
|
||||
function logWarning(message: string): void {
|
||||
const warningPrefix = '[warning]';
|
||||
core.info(`${warningPrefix}${message}`);
|
||||
}
|
||||
|
||||
run(true);
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as cache from '@actions/cache';
|
||||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import {supportedPackageManagers, PackageManagerInfo} from './package-managers';
|
||||
|
||||
export const getCommandOutput = async (toolCommand: string) => {
|
||||
let {stdout, stderr, exitCode} = await exec.getExecOutput(
|
||||
toolCommand,
|
||||
undefined,
|
||||
{ignoreReturnCode: true}
|
||||
);
|
||||
|
||||
if (exitCode) {
|
||||
stderr = !stderr.trim()
|
||||
? `The '${toolCommand}' command failed with exit code: ${exitCode}`
|
||||
: stderr;
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
return stdout.trim();
|
||||
};
|
||||
|
||||
export const getPackageManagerInfo = async (packageManager: string) => {
|
||||
if (!supportedPackageManagers[packageManager]) {
|
||||
throw new Error(
|
||||
`It's not possible to use ${packageManager}, please, check correctness of the package manager name spelling.`
|
||||
);
|
||||
}
|
||||
const obtainedPackageManager = supportedPackageManagers[packageManager];
|
||||
|
||||
return obtainedPackageManager;
|
||||
};
|
||||
|
||||
export const getCacheDirectoryPath = async (
|
||||
packageManagerInfo: PackageManagerInfo
|
||||
) => {
|
||||
const pathOutputs = await Promise.allSettled(
|
||||
packageManagerInfo.cacheFolderCommandList.map(async command =>
|
||||
getCommandOutput(command)
|
||||
)
|
||||
);
|
||||
|
||||
const results = pathOutputs.map(item => {
|
||||
if (item.status === 'fulfilled') {
|
||||
return item.value;
|
||||
} else {
|
||||
core.info(`[warning]getting cache directory path failed: ${item.reason}`);
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
const cachePaths = results.filter(item => item);
|
||||
|
||||
if (!cachePaths.length) {
|
||||
throw new Error(`Could not get cache folder paths.`);
|
||||
}
|
||||
|
||||
return cachePaths;
|
||||
};
|
||||
|
||||
export function isGhes(): boolean {
|
||||
const ghUrl = new URL(
|
||||
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
);
|
||||
|
||||
const hostname = ghUrl.hostname.trimEnd().toUpperCase();
|
||||
const isGitHubHost = hostname === 'GITHUB.COM';
|
||||
const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM');
|
||||
const isLocalHost = hostname.endsWith('.LOCALHOST');
|
||||
|
||||
return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost;
|
||||
}
|
||||
|
||||
export function isCacheFeatureAvailable(): boolean {
|
||||
if (cache.isFeatureAvailable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isGhes()) {
|
||||
core.warning(
|
||||
'Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
core.warning(
|
||||
'The runner was not able to contact the cache service. Caching will be skipped'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum State {
|
||||
CachePrimaryKey = 'CACHE_KEY',
|
||||
CacheMatchedKey = 'CACHE_RESULT'
|
||||
}
|
||||
|
||||
export enum Outputs {
|
||||
CacheHit = 'cache-hit'
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
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';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import {StableReleaseAlias, isSelfHosted} from './utils';
|
||||
|
||||
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`;
|
||||
|
||||
type InstallationType = 'dist' | 'manifest';
|
||||
|
||||
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 = os.arch()
|
||||
) {
|
||||
let manifest: tc.IToolRelease[] | undefined;
|
||||
const osPlat: string = os.platform();
|
||||
|
||||
if (
|
||||
versionSpec === StableReleaseAlias.Stable ||
|
||||
versionSpec === StableReleaseAlias.OldStable
|
||||
) {
|
||||
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) {
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
// check cache
|
||||
const toolPath = tc.find('go', 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;
|
||||
|
||||
//
|
||||
// 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 https://go.dev/dl
|
||||
//
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
return downloadPath;
|
||||
}
|
||||
|
||||
async function resolveVersionFromManifest(
|
||||
versionSpec: string,
|
||||
stable: boolean,
|
||||
auth: string | undefined,
|
||||
arch: string,
|
||||
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
|
||||
): Promise<string> {
|
||||
const tool = 'go';
|
||||
const version = makeSemver(info.resolvedVersion);
|
||||
return (
|
||||
(await cacheWindowsDir(extPath, tool, version, arch)) ||
|
||||
(await tc.cacheDir(extPath, tool, version, arch))
|
||||
);
|
||||
}
|
||||
|
||||
async function installGoVersion(
|
||||
info: IGoVersionInfo,
|
||||
auth: string | undefined,
|
||||
arch: string
|
||||
): 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');
|
||||
}
|
||||
|
||||
core.info('Adding to the cache ...');
|
||||
const toolCacheDir = await addExecutablesToToolCache(extPath, info, arch);
|
||||
core.info(`Successfully cached go to ${toolCacheDir}`);
|
||||
|
||||
return toolCacheDir;
|
||||
}
|
||||
|
||||
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 = os.arch(),
|
||||
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: string
|
||||
): Promise<IGoVersionInfo | null> {
|
||||
const version: IGoVersion | undefined = await findMatch(versionSpec, arch);
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const downloadUrl = `https://go.dev/dl/${version.files[0].filename}`;
|
||||
|
||||
return <IGoVersionInfo>{
|
||||
type: 'dist',
|
||||
downloadUrl: downloadUrl,
|
||||
resolvedVersion: version.version,
|
||||
fileName: version.files[0].filename
|
||||
};
|
||||
}
|
||||
|
||||
export async function findMatch(
|
||||
versionSpec: string,
|
||||
arch = os.arch()
|
||||
): Promise<IGoVersion | undefined> {
|
||||
const archFilter = sys.getArch(arch);
|
||||
const platFilter = sys.getPlatform();
|
||||
|
||||
let result: IGoVersion | undefined;
|
||||
let match: IGoVersion | undefined;
|
||||
|
||||
const dlUrl = 'https://golang.org/dl/?mode=json&include=all';
|
||||
const candidates: IGoVersion[] | null = await module.exports.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;
|
||||
}
|
||||
|
||||
export async function getVersionsDist(
|
||||
dlUrl: string
|
||||
): Promise<IGoVersion[] | null> {
|
||||
// this returns versions descending so latest is first
|
||||
const http: httpm.HttpClient = new httpm.HttpClient('setup-go', [], {
|
||||
allowRedirects: true,
|
||||
maxRedirects: 3
|
||||
});
|
||||
return (await http.getJson<IGoVersion[]>(dlUrl)).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'
|
||||
) {
|
||||
const match = contents.match(/^go (\d+(\.\d+)*)/m);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
return contents.trim();
|
||||
}
|
||||
|
||||
async function resolveStableVersionDist(versionSpec: string, arch: string) {
|
||||
const archFilter = sys.getArch(arch);
|
||||
const platFilter = sys.getPlatform();
|
||||
const dlUrl = 'https://golang.org/dl/?mode=json&include=all';
|
||||
const candidates: IGoVersion[] | null = await module.exports.getVersionsDist(
|
||||
dlUrl
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as io from '@actions/io';
|
||||
import * as installer from './installer';
|
||||
import * as semver from 'semver';
|
||||
import path from 'path';
|
||||
import {restoreCache} from './cache-restore';
|
||||
import {isCacheFeatureAvailable} from './cache-utils';
|
||||
import cp from 'child_process';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
|
||||
export async function run() {
|
||||
try {
|
||||
//
|
||||
// versionSpec is optional. If supplied, install / use from the tool cache
|
||||
// If not supplied then problem matchers will still be setup. Useful for self-hosted.
|
||||
//
|
||||
const versionSpec = resolveVersionInput();
|
||||
|
||||
const cache = core.getBooleanInput('cache');
|
||||
core.info(`Setup go version spec ${versionSpec}`);
|
||||
|
||||
let arch = core.getInput('architecture');
|
||||
|
||||
if (!arch) {
|
||||
arch = os.arch();
|
||||
}
|
||||
|
||||
if (versionSpec) {
|
||||
const token = core.getInput('token');
|
||||
const auth = !token ? undefined : `token ${token}`;
|
||||
|
||||
const checkLatest = core.getBooleanInput('check-latest');
|
||||
|
||||
const installDir = await installer.getGo(
|
||||
versionSpec,
|
||||
checkLatest,
|
||||
auth,
|
||||
arch
|
||||
);
|
||||
|
||||
const installDirVersion = path.basename(path.dirname(installDir));
|
||||
|
||||
core.addPath(path.join(installDir, 'bin'));
|
||||
core.info('Added go to the path');
|
||||
|
||||
const version = installer.makeSemver(installDirVersion);
|
||||
// Go versions less than 1.9 require GOROOT to be set
|
||||
if (semver.lt(version, '1.9.0')) {
|
||||
core.info('Setting GOROOT for Go version < 1.9');
|
||||
core.exportVariable('GOROOT', installDir);
|
||||
}
|
||||
|
||||
core.info(`Successfully set up Go version ${versionSpec}`);
|
||||
} else {
|
||||
core.info(
|
||||
'[warning]go-version input was not specified. The action will try to use pre-installed version.'
|
||||
);
|
||||
}
|
||||
|
||||
const added = await addBinToPath();
|
||||
core.debug(`add bin ${added}`);
|
||||
|
||||
const goPath = await io.which('go');
|
||||
const goVersion = (cp.execSync(`${goPath} version`) || '').toString();
|
||||
|
||||
if (cache && isCacheFeatureAvailable()) {
|
||||
const packageManager = 'default';
|
||||
const cacheDependencyPath = core.getInput('cache-dependency-path');
|
||||
try {
|
||||
await restoreCache(
|
||||
parseGoVersion(goVersion),
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
} catch (error) {
|
||||
core.warning(`Restore cache failed: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// add problem matchers
|
||||
const matchersPath = path.join(__dirname, '../..', 'matchers.json');
|
||||
core.info(`##[add-matcher]${matchersPath}`);
|
||||
|
||||
// output the version actually being used
|
||||
core.info(goVersion);
|
||||
|
||||
core.setOutput('go-version', parseGoVersion(goVersion));
|
||||
|
||||
core.startGroup('go env');
|
||||
const goEnv = (cp.execSync(`${goPath} env`) || '').toString();
|
||||
core.info(goEnv);
|
||||
core.endGroup();
|
||||
} catch (error) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addBinToPath(): Promise<boolean> {
|
||||
let added = false;
|
||||
const g = await io.which('go');
|
||||
core.debug(`which go :${g}:`);
|
||||
if (!g) {
|
||||
core.debug('go not in the path');
|
||||
return added;
|
||||
}
|
||||
|
||||
const buf = cp.execSync('go env GOPATH');
|
||||
if (buf.length > 1) {
|
||||
const gp = buf.toString().trim();
|
||||
core.debug(`go env GOPATH :${gp}:`);
|
||||
if (!fs.existsSync(gp)) {
|
||||
// some of the hosted images have go install but not profile dir
|
||||
core.debug(`creating ${gp}`);
|
||||
await io.mkdirP(gp);
|
||||
}
|
||||
|
||||
const bp = path.join(gp, 'bin');
|
||||
if (!fs.existsSync(bp)) {
|
||||
core.debug(`creating ${bp}`);
|
||||
await io.mkdirP(bp);
|
||||
}
|
||||
|
||||
core.addPath(bp);
|
||||
added = true;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
export function parseGoVersion(versionString: string): string {
|
||||
// get the installed version as an Action output
|
||||
// based on go/src/cmd/go/internal/version/version.go:
|
||||
// fmt.Printf("go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
// expecting go<version> for runtime.Version()
|
||||
return versionString.split(' ')[2].slice('go'.length);
|
||||
}
|
||||
|
||||
function resolveVersionInput(): string {
|
||||
let version = core.getInput('go-version');
|
||||
const versionFilePath = core.getInput('go-version-file');
|
||||
|
||||
if (version && versionFilePath) {
|
||||
core.warning(
|
||||
'Both go-version and go-version-file inputs are specified, only go-version will be used'
|
||||
);
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return version;
|
||||
}
|
||||
|
||||
if (versionFilePath) {
|
||||
if (!fs.existsSync(versionFilePath)) {
|
||||
throw new Error(
|
||||
`The specified go version file at: ${versionFilePath} does not exist`
|
||||
);
|
||||
}
|
||||
version = installer.parseGoVersionFile(versionFilePath);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
type SupportedPackageManagers = {
|
||||
[prop: string]: PackageManagerInfo;
|
||||
};
|
||||
|
||||
export interface PackageManagerInfo {
|
||||
dependencyFilePattern: string;
|
||||
cacheFolderCommandList: string[];
|
||||
}
|
||||
|
||||
export const supportedPackageManagers: SupportedPackageManagers = {
|
||||
default: {
|
||||
dependencyFilePattern: 'go.sum',
|
||||
cacheFolderCommandList: ['go env GOMODCACHE', 'go env GOCACHE']
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import {run} from './main';
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,39 @@
|
||||
import os from 'os';
|
||||
|
||||
export function getPlatform(): string {
|
||||
// darwin and linux match already
|
||||
// freebsd not supported yet but future proofed.
|
||||
|
||||
// 'aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', and 'win32'
|
||||
let plat: string = os.platform();
|
||||
|
||||
// wants 'darwin', 'freebsd', 'linux', 'windows'
|
||||
if (plat === 'win32') {
|
||||
plat = 'windows';
|
||||
}
|
||||
|
||||
return plat;
|
||||
}
|
||||
|
||||
export function getArch(arch: string): string {
|
||||
// 'arm', 'arm64', 'ia32', 'mips', 'mipsel', 'ppc', 'ppc64', 's390', 's390x', 'x32', and 'x64'.
|
||||
|
||||
// wants amd64, 386, arm64, armv61, ppc641e, s390x
|
||||
// currently not supported by runner but future proofed mapping
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
arch = 'amd64';
|
||||
break;
|
||||
// case 'ppc':
|
||||
// arch = 'ppc64';
|
||||
// break;
|
||||
case 'x32':
|
||||
arch = '386';
|
||||
break;
|
||||
case 'arm':
|
||||
arch = 'armv6l';
|
||||
break;
|
||||
}
|
||||
|
||||
return arch;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export enum StableReleaseAlias {
|
||||
Stable = 'stable',
|
||||
OldStable = 'oldstable'
|
||||
}
|
||||
|
||||
export const isSelfHosted = (): boolean =>
|
||||
process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' &&
|
||||
(process.env['AGENT_ISSELFHOSTED'] === '1' ||
|
||||
process.env['AGENT_ISSELFHOSTED'] === undefined);
|
||||
/* the above is simplified from:
|
||||
process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' && process.env['AGENT_ISSELFHOSTED'] === '1'
|
||||
||
|
||||
process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' && process.env['AGENT_ISSELFHOSTED'] === undefined
|
||||
*/
|
||||
Reference in New Issue
Block a user