Automatic caching of dependencies

in a best effort manner by default
allowing to specify files to hash for computing the cache key
This commit is contained in:
Paul Merlin 2020-06-15 12:59:55 +02:00
parent fcc1683d01
commit 95e20daa83
15 changed files with 192 additions and 14 deletions

72
src/cache-dependencies.ts Normal file
View file

@ -0,0 +1,72 @@
import * as path from 'path'
import * as fs from 'fs'
import * as os from 'os'
import * as core from '@actions/core'
import * as cache from '@actions/cache'
import * as github from './github-utils'
import * as crypto from './crypto-utils'
const DEPENDENCIES_CACHE_PATH = 'DEPENDENCIES_CACHE_PATH'
const DEPENDENCIES_CACHE_KEY = 'DEPENDENCIES_CACHE_KEY'
const DEPENDENCIES_CACHE_RESULT = 'DEPENDENCIES_CACHE_RESULT'
export async function restoreCachedDependencies(
rootDir: string
): Promise<void> {
const cachePath = path.resolve(os.homedir(), '.gradle/caches/modules-2')
core.saveState(DEPENDENCIES_CACHE_PATH, cachePath)
const inputCacheKeyGlobs = github.inputArrayOrNull('dependencies-cache-key')
const cacheKeyGlobs = inputCacheKeyGlobs
? inputCacheKeyGlobs
: [
'**/*.gradle',
'**/*.gradle.kts',
'**/gradle.properties',
'gradle/**'
]
const hash = await crypto.hashFiles(rootDir, cacheKeyGlobs)
const cacheKeyPrefix = 'dependencies-'
const cacheKey = `${cacheKeyPrefix}${hash}`
core.saveState(DEPENDENCIES_CACHE_KEY, cacheKey)
const cacheResult = await cache.restoreCache([cachePath], cacheKey, [
cacheKeyPrefix
])
core.saveState(DEPENDENCIES_CACHE_RESULT, cacheResult)
}
export async function cacheDependencies(): Promise<void> {
const cachePath = core.getState(DEPENDENCIES_CACHE_PATH)
const cacheKey = core.getState(DEPENDENCIES_CACHE_KEY)
const cacheResult = core.getState(DEPENDENCIES_CACHE_RESULT)
if (!cachePath || !fs.existsSync(cachePath)) {
core.debug('No dependencies to cache.')
return
}
if (cacheResult && cacheKey === cacheResult) {
core.info(
`Dependencies cache hit occurred on the cache key ${cacheKey}, not saving cache.`
)
return
}
try {
await cache.saveCache([cachePath], cacheKey)
} catch (error) {
if (error.name === cache.ValidationError.name) {
throw error
} else if (error.name === cache.ReserveCacheError.name) {
core.info(error.message)
} else {
core.info(`[warning] ${error.message}`)
}
}
return
}

View file

@ -52,7 +52,7 @@ export async function cacheWrapperDist(): Promise<void> {
const cachePath = core.getState(WRAPPER_CACHE_PATH)
const cacheResult = core.getState(WRAPPER_CACHE_RESULT)
if (!cachePath) {
if (!cachePath || !fs.existsSync(cachePath)) {
core.debug('No wrapper installation to cache.')
return
}

40
src/crypto-utils.ts Normal file
View file

@ -0,0 +1,40 @@
import * as crypto from 'crypto'
import * as fs from 'fs'
import * as path from 'path'
import * as stream from 'stream'
import * as util from 'util'
import * as glob from '@actions/glob'
export async function hashFiles(
baseDir: string,
globs: string[] = ['**'],
followSymbolicLinks = false
): Promise<string | null> {
let hasMatch = false
const result = crypto.createHash('sha256')
for await (const globPattern of globs) {
const globMatch = `${baseDir}/${globPattern}`
const globber = await glob.create(globMatch, {followSymbolicLinks})
for await (const file of globber.globGenerator()) {
// console.log(file)
if (!file.startsWith(`${baseDir}${path.sep}`)) {
// console.log(`Ignore '${file}' since it is not under '${baseDir}'.`)
continue
}
if (fs.statSync(file).isDirectory()) {
// console.log(`Skip directory '${file}'.`)
continue
}
const hash = crypto.createHash('sha256')
const pipeline = util.promisify(stream.pipeline)
await pipeline(fs.createReadStream(file), hash)
result.write(hash.digest())
if (!hasMatch) {
hasMatch = true
}
}
}
result.end()
return hasMatch ? result.digest('hex') : null
}

View file

@ -1,10 +1,13 @@
import * as exec from '@actions/exec'
import * as cacheDependencies from './cache-dependencies'
export async function execute(
executable: string,
root: string,
argv: string[]
): Promise<BuildResult> {
await cacheDependencies.restoreCachedDependencies(root)
let publishing = false
let buildScanUrl: string | undefined

View file

@ -7,3 +7,12 @@ export function inputOrNull(name: string): string | null {
}
return inputString
}
export function inputArrayOrNull(name: string): string[] | null {
const string = inputOrNull(name)
if (!string) return null
return string
.split('\n')
.map(s => s.trim())
.filter(s => s !== '')
}

View file

@ -1,8 +1,10 @@
import * as cacheWrapper from './cache-wrapper'
import * as cacheDependencies from './cache-dependencies'
// Invoked by GitHub Actions
export async function run(): Promise<void> {
await cacheWrapper.cacheWrapperDist()
await cacheDependencies.cacheDependencies()
}
run()