diff --git a/action.yml b/action.yml index 907b028..d51572a 100644 --- a/action.yml +++ b/action.yml @@ -14,7 +14,7 @@ inputs: description: Used to pull node distributions from go-versions. Since there's a default, this is typically not supplied by the user. default: ${{ github.token }} cache: - description: 'Used to specify whether go-modules caching is needed or not. Supported values: true, false.' + description: 'Used to specify whether go-modules caching is needed. Set to true, if you'd like to enable caching.' cache-dependency-path: description: 'Used to specify the path to a dependency file: go.sum.' runs: diff --git a/package.json b/package.json index fd98837..78bac02 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,10 @@ "author": "GitHub", "license": "MIT", "dependencies": { + "@actions/cache": "^1.0.8", "@actions/core": "^1.6.0", + "@actions/exec": "^1.1.0", + "@actions/glob": "^0.2.0", "@actions/http-client": "^1.0.6", "@actions/io": "^1.0.2", "@actions/tool-cache": "^1.5.5", diff --git a/src/cache-restore.ts b/src/cache-restore.ts new file mode 100644 index 0000000..33d7283 --- /dev/null +++ b/src/cache-restore.ts @@ -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 { + getCacheDirectoryPath, + getPackageManagerInfo, + PackageManagerInfo +} from './cache-utils'; + +export const restoreCache = async ( + packageManager: string, + cacheDependencyPath?: string +) => { + const packageManagerInfo = await getPackageManagerInfo(); + const platform = process.env.RUNNER_OS; + + const cachePath = await getCacheDirectoryPath( + packageManagerInfo + ); + + const goSumFilePath = cacheDependencyPath + ? cacheDependencyPath + : findGoSumFile(packageManagerInfo); + const fileHash = await glob.hashFiles(goSumFilePath); + + if (!fileHash) { + throw new Error( + 'Some specified paths were not resolved, unable to cache dependencies.' + ); + } + + const primaryKey = `go-cache-${platform}-${fileHash}`; + core.debug(`primary key is ${primaryKey}`); + + core.saveState(State.CachePrimaryKey, primaryKey); + + const cacheKey = await cache.restoreCache([cachePath], primaryKey); + core.setOutput('cache-hit', Boolean(cacheKey)); + + if (!cacheKey) { + core.info(`${packageManager} cache is not found`); + return; + } + + core.saveState(State.CacheMatchedKey, cacheKey); + core.info(`Cache restored from key: ${cacheKey}`); +}; + +const findGoSumFile = (packageManager: PackageManagerInfo) => { + let goSumFile = packageManager.goSumFilePattern; + const workspace = process.env.GITHUB_WORKSPACE!; + const rootContent = fs.readdirSync(workspace); + + const goSumFileExists = rootContent.includes(goSumFile); + if (!goSumFileExists) { + throw new Error( + `Dependencies file go.sum is not found in ${workspace}. Supported file pattern: ${goSumFile}` + ); + } + + return path.join(workspace, goSumFile); +}; diff --git a/src/cache-save.ts b/src/cache-save.ts new file mode 100644 index 0000000..578df0b --- /dev/null +++ b/src/cache-save.ts @@ -0,0 +1,60 @@ +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}`); +}); + +export async function run() { + try { + await cachePackages(); + } catch (error) { + core.setFailed(error.message); + } +} + +const cachePackages = async () => { + const state = core.getState(State.CacheMatchedKey); + const primaryKey = core.getState(State.CachePrimaryKey); + + const packageManagerInfo = await getPackageManagerInfo(); + + const cachePath = await getCacheDirectoryPath( + packageManagerInfo, + ); + + if (!fs.existsSync(cachePath)) { + throw new Error( + `Cache folder path is retrieved but doesn't exist on disk: ${cachePath}` + ); + } + + if (primaryKey === state) { + core.info( + `Cache hit occurred on the primary key ${primaryKey}, not saving cache.` + ); + return; + } + + try { + await cache.saveCache([cachePath], primaryKey); + core.info(`Cache saved with the key: ${primaryKey}`); + } catch (error) { + if (error.name === cache.ValidationError.name) { + throw error; + } else if (error.name === cache.ReserveCacheError.name) { + core.info(error.message); + } else { + core.warning(`${error.message}`); + } + } +}; + +run(); diff --git a/src/cache-utils.ts b/src/cache-utils.ts new file mode 100644 index 0000000..b3ad60d --- /dev/null +++ b/src/cache-utils.ts @@ -0,0 +1,49 @@ +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; + +export interface PackageManagerInfo { + goSumFilePattern: string; + getCacheFolderCommand: string; +} + +export const defaultPackageManager: PackageManagerInfo = { + goSumFilePattern: 'go.sum', + getCacheFolderCommand: 'go env GOMODCACHE', +}; + +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 () => { + + return defaultPackageManager; + +}; + +export const getCacheDirectoryPath = async ( + packageManagerInfo: PackageManagerInfo, +) => { + const stdout = await getCommandOutput( + packageManagerInfo.getCacheFolderCommand + ); + + if (!stdout) { + throw new Error(`Could not get cache folder path.`); + } + + return stdout; +}; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..b43d18c --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +export enum State { + CachePrimaryKey = 'CACHE_KEY', + CacheMatchedKey = 'CACHE_RESULT' +} + +export enum Outputs { + CacheHit = 'cache-hit' +} diff --git a/src/main.ts b/src/main.ts index 5837dee..bfb8543 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import * as core from '@actions/core'; import * as io from '@actions/io'; import * as installer from './installer'; import path from 'path'; +import {restoreCache} from './cache-restore'; import cp from 'child_process'; import fs from 'fs'; import {URL} from 'url'; @@ -13,10 +14,11 @@ export async function run() { // If not supplied then problem matchers will still be setup. Useful for self-hosted. // let versionSpec = core.getInput('go-version'); - + // stable will be true unless false is the exact input // since getting unstable versions should be explicit let stable = (core.getInput('stable') || 'true').toUpperCase() === 'TRUE'; + const cache = core.getInput('cache'); core.info(`Setup go ${stable ? 'stable' : ''} version spec ${versionSpec}`); @@ -41,6 +43,14 @@ export async function run() { core.info(`Successfully setup go version ${versionSpec}`); } + if (cache) { + if (isGhes()) { + throw new Error('Caching is not supported on GHES'); + } + const cacheDependencyPath = core.getInput('cache-dependency-path'); + await restoreCache(cache, cacheDependencyPath); + } + // add problem matchers const matchersPath = path.join(__dirname, '..', 'matchers.json'); core.info(`##[add-matcher]${matchersPath}`);