mirror of
https://github.com/gradle/gradle-build-action.git
synced 2024-11-26 02:52:09 +00:00
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:
parent
fcc1683d01
commit
95e20daa83
15 changed files with 192 additions and 14 deletions
4
.github/workflows/dev.yml
vendored
4
.github/workflows/dev.yml
vendored
|
@ -22,10 +22,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
wrapper-directory: __tests__/data/basic
|
wrapper-directory: __tests__/data/basic
|
||||||
build-root-directory: __tests__/data/basic
|
build-root-directory: __tests__/data/basic
|
||||||
arguments: help
|
arguments: test
|
||||||
- name: Test dist download
|
- name: Test dist download
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.5
|
gradle-version: 6.5
|
||||||
build-root-directory: __tests__/data/basic
|
build-root-directory: __tests__/data/basic
|
||||||
arguments: help
|
arguments: test
|
||||||
|
|
4
.github/workflows/prod.yml
vendored
4
.github/workflows/prod.yml
vendored
|
@ -21,10 +21,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
wrapper-directory: __tests__/data/basic
|
wrapper-directory: __tests__/data/basic
|
||||||
build-root-directory: __tests__/data/basic
|
build-root-directory: __tests__/data/basic
|
||||||
arguments: help
|
arguments: test
|
||||||
- name: Test dist download
|
- name: Test dist download
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.5
|
gradle-version: 6.5
|
||||||
build-root-directory: __tests__/data/basic
|
build-root-directory: __tests__/data/basic
|
||||||
arguments: help
|
arguments: test
|
||||||
|
|
33
__tests__/crypto-utils.test.ts
Normal file
33
__tests__/crypto-utils.test.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import * as cryptoUtils from '../src/crypto-utils'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
describe('crypto-utils', () => {
|
||||||
|
describe('can hash', () => {
|
||||||
|
it('a directory', async () => {
|
||||||
|
const hash = await cryptoUtils.hashFiles(
|
||||||
|
path.resolve('__tests__/data/basic/gradle')
|
||||||
|
)
|
||||||
|
expect(hash).toBe(
|
||||||
|
'4ebb65b45e6f6796d5ec6ace96e9471cc6573d294c54f99c4920fe5328e75bab'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('a directory with a glob', async () => {
|
||||||
|
const hash = await cryptoUtils.hashFiles(
|
||||||
|
path.resolve('__tests__/data/basic/'),
|
||||||
|
['gradle/**']
|
||||||
|
)
|
||||||
|
expect(hash).toBe(
|
||||||
|
'4ebb65b45e6f6796d5ec6ace96e9471cc6573d294c54f99c4920fe5328e75bab'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('a directory with globs', async () => {
|
||||||
|
const hash = await cryptoUtils.hashFiles(
|
||||||
|
path.resolve('__tests__/data/basic/'),
|
||||||
|
['**/*.gradle', 'gradle/**']
|
||||||
|
)
|
||||||
|
expect(hash).toBe(
|
||||||
|
'2db1d5291774949ab89e18e9d82ee24748ca0f6cc78de69ea9104357c50ad4a5'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,6 +1,11 @@
|
||||||
/*
|
plugins {
|
||||||
* This file was generated by the Gradle 'init' task.
|
id 'java'
|
||||||
*
|
}
|
||||||
* This is a general purpose Gradle build.
|
|
||||||
* Learn how to create Gradle builds at https://guides.gradle.org/creating-new-gradle-builds
|
repositories {
|
||||||
*/
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation('junit:junit:4.12')
|
||||||
|
}
|
||||||
|
|
10
__tests__/data/basic/src/test/java/basic/BasicTest.java
Normal file
10
__tests__/data/basic/src/test/java/basic/BasicTest.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package basic;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class BasicTest {
|
||||||
|
@Test
|
||||||
|
public void test() {
|
||||||
|
assert true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,9 @@ inputs:
|
||||||
arguments:
|
arguments:
|
||||||
description: Gradle command line arguments, see gradle --help
|
description: Gradle command line arguments, see gradle --help
|
||||||
required: false
|
required: false
|
||||||
|
dependencies-cache-key:
|
||||||
|
description: Globs of files to hash in the build root directory, separated by new lines, use best-effort if unset
|
||||||
|
required: false
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
build-scan-url:
|
build-scan-url:
|
||||||
|
|
2
dist/main/index.js
vendored
2
dist/main/index.js
vendored
File diff suppressed because one or more lines are too long
2
dist/post/index.js
vendored
2
dist/post/index.js
vendored
File diff suppressed because one or more lines are too long
|
@ -24,11 +24,12 @@
|
||||||
"author": "Paul Merlin <paul@nosphere.org>",
|
"author": "Paul Merlin <paul@nosphere.org>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@actions/cache": "0.2.1",
|
||||||
"@actions/core": "1.2.4",
|
"@actions/core": "1.2.4",
|
||||||
"@actions/exec": "1.0.4",
|
"@actions/exec": "1.0.4",
|
||||||
|
"@actions/glob": "0.1.0",
|
||||||
"@actions/io": "1.0.2",
|
"@actions/io": "1.0.2",
|
||||||
"@actions/tool-cache": "1.5.5",
|
"@actions/tool-cache": "1.5.5",
|
||||||
"@actions/cache": "0.2.1",
|
|
||||||
"string-argv": "0.3.1",
|
"string-argv": "0.3.1",
|
||||||
"typed-rest-client": "1.7.3",
|
"typed-rest-client": "1.7.3",
|
||||||
"unzipper": "0.10.11"
|
"unzipper": "0.10.11"
|
||||||
|
|
72
src/cache-dependencies.ts
Normal file
72
src/cache-dependencies.ts
Normal 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
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ export async function cacheWrapperDist(): Promise<void> {
|
||||||
const cachePath = core.getState(WRAPPER_CACHE_PATH)
|
const cachePath = core.getState(WRAPPER_CACHE_PATH)
|
||||||
const cacheResult = core.getState(WRAPPER_CACHE_RESULT)
|
const cacheResult = core.getState(WRAPPER_CACHE_RESULT)
|
||||||
|
|
||||||
if (!cachePath) {
|
if (!cachePath || !fs.existsSync(cachePath)) {
|
||||||
core.debug('No wrapper installation to cache.')
|
core.debug('No wrapper installation to cache.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
40
src/crypto-utils.ts
Normal file
40
src/crypto-utils.ts
Normal 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
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
import * as exec from '@actions/exec'
|
import * as exec from '@actions/exec'
|
||||||
|
import * as cacheDependencies from './cache-dependencies'
|
||||||
|
|
||||||
export async function execute(
|
export async function execute(
|
||||||
executable: string,
|
executable: string,
|
||||||
root: string,
|
root: string,
|
||||||
argv: string[]
|
argv: string[]
|
||||||
): Promise<BuildResult> {
|
): Promise<BuildResult> {
|
||||||
|
await cacheDependencies.restoreCachedDependencies(root)
|
||||||
|
|
||||||
let publishing = false
|
let publishing = false
|
||||||
let buildScanUrl: string | undefined
|
let buildScanUrl: string | undefined
|
||||||
|
|
||||||
|
|
|
@ -7,3 +7,12 @@ export function inputOrNull(name: string): string | null {
|
||||||
}
|
}
|
||||||
return inputString
|
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 !== '')
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import * as cacheWrapper from './cache-wrapper'
|
import * as cacheWrapper from './cache-wrapper'
|
||||||
|
import * as cacheDependencies from './cache-dependencies'
|
||||||
|
|
||||||
// Invoked by GitHub Actions
|
// Invoked by GitHub Actions
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await cacheWrapper.cacheWrapperDist()
|
await cacheWrapper.cacheWrapperDist()
|
||||||
|
await cacheDependencies.cacheDependencies()
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|
Loading…
Reference in a new issue