mirror of
https://github.com/gradle/gradle-build-action.git
synced 2024-11-26 18:21:05 -05:00
Merge pull request #128 from gradle/configuration-caching
Restore/save configuration-cache data in first action step. This will enable the use of the action for caching without managing all gradle invocations.
This commit is contained in:
commit
367ce74a5f
14 changed files with 311 additions and 233 deletions
|
@ -26,8 +26,13 @@ jobs:
|
||||||
with:
|
with:
|
||||||
build-root-directory: __tests__/samples/groovy-dsl
|
build-root-directory: __tests__/samples/groovy-dsl
|
||||||
arguments: test --configuration-cache
|
arguments: test --configuration-cache
|
||||||
|
- name: Second build with configuration-cache enabled
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
build-root-directory: __tests__/samples/kotlin-dsl
|
||||||
|
arguments: test --configuration-cache
|
||||||
|
|
||||||
# Test that the project-dot-gradle cache will cache and restore configuration-cache
|
# Test restore configuration-cache
|
||||||
configuration-cache:
|
configuration-cache:
|
||||||
needs: seed-build
|
needs: seed-build
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -46,6 +51,25 @@ jobs:
|
||||||
arguments: test --configuration-cache
|
arguments: test --configuration-cache
|
||||||
cache-read-only: true
|
cache-read-only: true
|
||||||
|
|
||||||
|
# Test restore configuration-cache from second build invocation
|
||||||
|
configuration-cache-2:
|
||||||
|
needs: seed-build
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Execute Gradle build and verify cached configuration
|
||||||
|
uses: ./
|
||||||
|
env:
|
||||||
|
VERIFY_CACHED_CONFIGURATION: true
|
||||||
|
with:
|
||||||
|
build-root-directory: __tests__/samples/kotlin-dsl
|
||||||
|
arguments: test --configuration-cache
|
||||||
|
cache-read-only: true
|
||||||
|
|
||||||
# Check that the build can run when no extracted cache entries are restored
|
# Check that the build can run when no extracted cache entries are restored
|
||||||
no-extracted-cache-entries-restored:
|
no-extracted-cache-entries-restored:
|
||||||
needs: seed-build
|
needs: seed-build
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {CacheEntryListener, CacheListener} from '../src/cache-base'
|
import {CacheEntryListener, CacheListener} from '../src/cache-reporting'
|
||||||
|
|
||||||
describe('caching report', () => {
|
describe('caching report', () => {
|
||||||
describe('reports not fully restored', () => {
|
describe('reports not fully restored', () => {
|
|
@ -16,3 +16,15 @@ dependencies {
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named("test").configure {
|
||||||
|
// Use an environment variable to bypass config-cache checks
|
||||||
|
if (System.getenv("VERIFY_CACHED_CONFIGURATION") != null) {
|
||||||
|
throw RuntimeException("Configuration was not cached: unexpected configuration of test task")
|
||||||
|
}
|
||||||
|
doLast {
|
||||||
|
if (System.getProperties().containsKey("verifyCachedBuild")) {
|
||||||
|
throw RuntimeException("Build was not cached: unexpected execution of test task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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/main/index.js.map
vendored
2
dist/main/index.js.map
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
2
dist/post/index.js.map
vendored
2
dist/post/index.js.map
vendored
File diff suppressed because one or more lines are too long
|
@ -1,10 +1,15 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as cache from '@actions/cache'
|
import * as cache from '@actions/cache'
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import {isCacheDebuggingEnabled, getCacheKeyPrefix, hashStrings, handleCacheFailure} from './cache-utils'
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
import {CacheListener} from './cache-reporting'
|
||||||
|
import {isCacheDebuggingEnabled, getCacheKeyPrefix, determineJobContext, handleCacheFailure} from './cache-utils'
|
||||||
|
|
||||||
const CACHE_PROTOCOL_VERSION = 'v5-'
|
const CACHE_PROTOCOL_VERSION = 'v5-'
|
||||||
const JOB_CONTEXT_PARAMETER = 'workflow-job-context'
|
|
||||||
|
export const META_FILE_DIR = '.gradle-build-action'
|
||||||
|
export const PROJECT_ROOTS_FILE = 'project-roots.txt'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a key used to restore a cache entry.
|
* Represents a key used to restore a cache entry.
|
||||||
|
@ -57,100 +62,17 @@ function generateCacheKey(cacheName: string): CacheKey {
|
||||||
return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs])
|
return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs])
|
||||||
}
|
}
|
||||||
|
|
||||||
function determineJobContext(): string {
|
|
||||||
// By default, we hash the full `matrix` data for the run, to uniquely identify this job invocation
|
|
||||||
// The only way we can obtain the `matrix` data is via the `workflow-job-context` parameter in action.yml.
|
|
||||||
const workflowJobContext = core.getInput(JOB_CONTEXT_PARAMETER)
|
|
||||||
return hashStrings([workflowJobContext])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects information on what entries were saved and restored during the action.
|
|
||||||
* This information is used to generate a summary of the cache usage.
|
|
||||||
*/
|
|
||||||
export class CacheListener {
|
|
||||||
cacheEntries: CacheEntryListener[] = []
|
|
||||||
|
|
||||||
get fullyRestored(): boolean {
|
|
||||||
return this.cacheEntries.every(x => !x.wasRequestedButNotRestored())
|
|
||||||
}
|
|
||||||
|
|
||||||
entry(name: string): CacheEntryListener {
|
|
||||||
for (const entry of this.cacheEntries) {
|
|
||||||
if (entry.entryName === name) {
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newEntry = new CacheEntryListener(name)
|
|
||||||
this.cacheEntries.push(newEntry)
|
|
||||||
return newEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
stringify(): string {
|
|
||||||
return JSON.stringify(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
static rehydrate(stringRep: string): CacheListener {
|
|
||||||
const rehydrated: CacheListener = Object.assign(new CacheListener(), JSON.parse(stringRep))
|
|
||||||
const entries = rehydrated.cacheEntries
|
|
||||||
for (let index = 0; index < entries.length; index++) {
|
|
||||||
const rawEntry = entries[index]
|
|
||||||
entries[index] = Object.assign(new CacheEntryListener(rawEntry.entryName), rawEntry)
|
|
||||||
}
|
|
||||||
return rehydrated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects information on the state of a single cache entry.
|
|
||||||
*/
|
|
||||||
export class CacheEntryListener {
|
|
||||||
entryName: string
|
|
||||||
requestedKey: string | undefined
|
|
||||||
requestedRestoreKeys: string[] | undefined
|
|
||||||
restoredKey: string | undefined
|
|
||||||
restoredSize: number | undefined
|
|
||||||
|
|
||||||
savedKey: string | undefined
|
|
||||||
savedSize: number | undefined
|
|
||||||
|
|
||||||
constructor(entryName: string) {
|
|
||||||
this.entryName = entryName
|
|
||||||
}
|
|
||||||
|
|
||||||
wasRequestedButNotRestored(): boolean {
|
|
||||||
return this.requestedKey !== undefined && this.restoredKey === undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
markRequested(key: string, restoreKeys: string[] = []): CacheEntryListener {
|
|
||||||
this.requestedKey = key
|
|
||||||
this.requestedRestoreKeys = restoreKeys
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
markRestored(key: string, size: number | undefined): CacheEntryListener {
|
|
||||||
this.restoredKey = key
|
|
||||||
this.restoredSize = size
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
markSaved(key: string, size: number | undefined): CacheEntryListener {
|
|
||||||
this.savedKey = key
|
|
||||||
this.savedSize = size
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class AbstractCache {
|
export abstract class AbstractCache {
|
||||||
private cacheName: string
|
private cacheName: string
|
||||||
private cacheDescription: string
|
private cacheDescription: string
|
||||||
private cacheKeyStateKey: string
|
private cacheKeyStateKey: string
|
||||||
private cacheResultStateKey: string
|
private cacheResultStateKey: string
|
||||||
|
|
||||||
|
protected readonly gradleUserHome: string
|
||||||
protected readonly cacheDebuggingEnabled: boolean
|
protected readonly cacheDebuggingEnabled: boolean
|
||||||
|
|
||||||
constructor(cacheName: string, cacheDescription: string) {
|
constructor(gradleUserHome: string, cacheName: string, cacheDescription: string) {
|
||||||
|
this.gradleUserHome = gradleUserHome
|
||||||
this.cacheName = cacheName
|
this.cacheName = cacheName
|
||||||
this.cacheDescription = cacheDescription
|
this.cacheDescription = cacheDescription
|
||||||
this.cacheKeyStateKey = `CACHE_KEY_${cacheName}`
|
this.cacheKeyStateKey = `CACHE_KEY_${cacheName}`
|
||||||
|
@ -158,23 +80,28 @@ export abstract class AbstractCache {
|
||||||
this.cacheDebuggingEnabled = isCacheDebuggingEnabled()
|
this.cacheDebuggingEnabled = isCacheDebuggingEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
const actionCacheDir = path.resolve(this.gradleUserHome, '.gradle-build-action')
|
||||||
|
fs.mkdirSync(actionCacheDir, {recursive: true})
|
||||||
|
|
||||||
|
const initScriptsDir = path.resolve(this.gradleUserHome, 'init.d')
|
||||||
|
fs.mkdirSync(initScriptsDir, {recursive: true})
|
||||||
|
|
||||||
|
this.initializeGradleUserHome(this.gradleUserHome, initScriptsDir)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores the cache entry, finding the closest match to the currently running job.
|
* Restores the cache entry, finding the closest match to the currently running job.
|
||||||
* If the target output already exists, caching will be skipped.
|
|
||||||
*/
|
*/
|
||||||
async restore(listener: CacheListener): Promise<void> {
|
async restore(listener: CacheListener): Promise<void> {
|
||||||
if (this.cacheOutputExists()) {
|
|
||||||
core.info(`${this.cacheDescription} already exists. Not restoring from cache.`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const entryListener = listener.entry(this.cacheDescription)
|
const entryListener = listener.entry(this.cacheDescription)
|
||||||
|
|
||||||
const cacheKey = this.prepareCacheKey()
|
const cacheKey = this.prepareCacheKey()
|
||||||
|
|
||||||
this.debug(
|
this.debug(
|
||||||
`Requesting ${this.cacheDescription} with
|
`Requesting ${this.cacheDescription} with
|
||||||
key:${cacheKey.key}
|
key:${cacheKey.key}
|
||||||
restoreKeys:[${cacheKey.restoreKeys}]`
|
restoreKeys:[${cacheKey.restoreKeys}]`
|
||||||
)
|
)
|
||||||
|
|
||||||
const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys)
|
const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys)
|
||||||
|
@ -219,28 +146,17 @@ export abstract class AbstractCache {
|
||||||
protected async afterRestore(_listener: CacheListener): Promise<void> {}
|
protected async afterRestore(_listener: CacheListener): Promise<void> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the cache entry based on the current cache key, unless:
|
* Saves the cache entry based on the current cache key unless the cache was restored with the exact key,
|
||||||
* - If the cache output existed before restore, then it is not saved.
|
* in which case we cannot overwrite it.
|
||||||
* - If the cache was restored with the exact key, we cannot overwrite it.
|
|
||||||
*
|
*
|
||||||
* If the cache entry was restored with a partial match on a restore key, then
|
* If the cache entry was restored with a partial match on a restore key, then
|
||||||
* it is saved with the exact key.
|
* it is saved with the exact key.
|
||||||
*/
|
*/
|
||||||
async save(listener: CacheListener): Promise<void> {
|
async save(listener: CacheListener): Promise<void> {
|
||||||
if (!this.cacheOutputExists()) {
|
|
||||||
core.info(`No ${this.cacheDescription} to cache.`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the state set in the previous 'restore' step.
|
// Retrieve the state set in the previous 'restore' step.
|
||||||
const cacheKeyFromRestore = core.getState(this.cacheKeyStateKey)
|
const cacheKeyFromRestore = core.getState(this.cacheKeyStateKey)
|
||||||
const cacheResultFromRestore = core.getState(this.cacheResultStateKey)
|
const cacheResultFromRestore = core.getState(this.cacheResultStateKey)
|
||||||
|
|
||||||
if (!cacheKeyFromRestore) {
|
|
||||||
core.info(`${this.cacheDescription} existed prior to cache restore. Not saving.`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheResultFromRestore && cacheKeyFromRestore === cacheResultFromRestore) {
|
if (cacheResultFromRestore && cacheKeyFromRestore === cacheResultFromRestore) {
|
||||||
core.info(`Cache hit occurred on the cache key ${cacheKeyFromRestore}, not saving cache.`)
|
core.info(`Cache hit occurred on the cache key ${cacheKeyFromRestore}, not saving cache.`)
|
||||||
return
|
return
|
||||||
|
@ -283,6 +199,6 @@ export abstract class AbstractCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract cacheOutputExists(): boolean
|
|
||||||
protected abstract getCachePath(): string[]
|
protected abstract getCachePath(): string[]
|
||||||
|
protected abstract initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import os from 'os'
|
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as glob from '@actions/glob'
|
import * as glob from '@actions/glob'
|
||||||
import * as exec from '@actions/exec'
|
import * as exec from '@actions/exec'
|
||||||
|
|
||||||
import {AbstractCache, CacheEntryListener, CacheListener} from './cache-base'
|
import {AbstractCache, META_FILE_DIR} from './cache-base'
|
||||||
|
import {CacheEntryListener, CacheListener} from './cache-reporting'
|
||||||
import {getCacheKeyPrefix, hashFileNames, tryDelete} from './cache-utils'
|
import {getCacheKeyPrefix, hashFileNames, tryDelete} from './cache-utils'
|
||||||
|
|
||||||
const META_FILE_DIR = '.gradle-build-action'
|
|
||||||
const META_FILE = 'cache-metadata.json'
|
const META_FILE = 'cache-metadata.json'
|
||||||
|
|
||||||
const INCLUDE_PATHS_PARAMETER = 'gradle-home-cache-includes'
|
const INCLUDE_PATHS_PARAMETER = 'gradle-home-cache-includes'
|
||||||
|
@ -46,16 +45,8 @@ class ExtractedCacheEntryMetadata {
|
||||||
* for more efficient storage.
|
* for more efficient storage.
|
||||||
*/
|
*/
|
||||||
export class GradleUserHomeCache extends AbstractCache {
|
export class GradleUserHomeCache extends AbstractCache {
|
||||||
private gradleUserHome: string
|
constructor(gradleUserHome: string) {
|
||||||
|
super(gradleUserHome, 'gradle', 'Gradle User Home')
|
||||||
constructor(rootDir: string) {
|
|
||||||
super('gradle', 'Gradle User Home')
|
|
||||||
this.gradleUserHome = this.determineGradleUserHome(rootDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(): void {
|
|
||||||
this.debug(`Initializing Gradle User Home with properties and init script: ${this.gradleUserHome}`)
|
|
||||||
initializeGradleUserHome(this.gradleUserHome)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -284,21 +275,6 @@ export class GradleUserHomeCache extends AbstractCache {
|
||||||
fs.writeFileSync(cacheMetadataFile, filedata, 'utf-8')
|
fs.writeFileSync(cacheMetadataFile, filedata, 'utf-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
protected determineGradleUserHome(rootDir: string): string {
|
|
||||||
const customGradleUserHome = process.env['GRADLE_USER_HOME']
|
|
||||||
if (customGradleUserHome) {
|
|
||||||
return path.resolve(rootDir, customGradleUserHome)
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.resolve(os.homedir(), '.gradle')
|
|
||||||
}
|
|
||||||
|
|
||||||
protected cacheOutputExists(): boolean {
|
|
||||||
// Need to check for 'caches' directory to avoid incorrect detection on MacOS agents
|
|
||||||
const dir = path.resolve(this.gradleUserHome, 'caches')
|
|
||||||
return fs.existsSync(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the paths within Gradle User Home to cache.
|
* Determines the paths within Gradle User Home to cache.
|
||||||
* By default, this is the 'caches' and 'notifications' directories,
|
* By default, this is the 'caches' and 'notifications' directories,
|
||||||
|
@ -363,21 +339,17 @@ export class GradleUserHomeCache extends AbstractCache {
|
||||||
|
|
||||||
core.info('-----------------------')
|
core.info('-----------------------')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function initializeGradleUserHome(gradleUserHome: string): void {
|
protected initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void {
|
||||||
fs.mkdirSync(gradleUserHome, {recursive: true})
|
const propertiesFile = path.resolve(gradleUserHome, 'gradle.properties')
|
||||||
|
fs.writeFileSync(propertiesFile, 'org.gradle.daemon=false')
|
||||||
|
|
||||||
const propertiesFile = path.resolve(gradleUserHome, 'gradle.properties')
|
const buildScanCapture = path.resolve(initScriptsDir, 'build-scan-capture.init.gradle')
|
||||||
fs.writeFileSync(propertiesFile, 'org.gradle.daemon=false')
|
fs.writeFileSync(
|
||||||
|
buildScanCapture,
|
||||||
|
`import org.gradle.util.GradleVersion
|
||||||
|
|
||||||
const initScript = path.resolve(gradleUserHome, 'init.gradle')
|
// Only run again root build. Do not run against included builds.
|
||||||
fs.writeFileSync(
|
|
||||||
initScript,
|
|
||||||
`
|
|
||||||
import org.gradle.util.GradleVersion
|
|
||||||
|
|
||||||
// Don't run against the included builds (if the main build has any).
|
|
||||||
def isTopLevelBuild = gradle.getParent() == null
|
def isTopLevelBuild = gradle.getParent() == null
|
||||||
if (isTopLevelBuild) {
|
if (isTopLevelBuild) {
|
||||||
def version = GradleVersion.current().baseVersion
|
def version = GradleVersion.current().baseVersion
|
||||||
|
@ -417,7 +389,7 @@ def registerCallbacks(buildScanExtension, rootProjectName) {
|
||||||
println("::set-output name=build-scan-url::\${buildScan.buildScanUri}")
|
println("::set-output name=build-scan-url::\${buildScan.buildScanUri}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}`
|
||||||
`
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,52 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import {AbstractCache} from './cache-base'
|
import {AbstractCache, META_FILE_DIR, PROJECT_ROOTS_FILE} from './cache-base'
|
||||||
|
|
||||||
// TODO: Maybe allow the user to override / tweak this set
|
|
||||||
const PATHS_TO_CACHE = [
|
|
||||||
'configuration-cache' // Only configuration-cache is stored at present
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple cache that saves and restores the '.gradle/configuration-cache' directory in the project root.
|
* A simple cache that saves and restores the '.gradle/configuration-cache' directory in the project root.
|
||||||
*/
|
*/
|
||||||
export class ProjectDotGradleCache extends AbstractCache {
|
export class ProjectDotGradleCache extends AbstractCache {
|
||||||
private rootDir: string
|
constructor(gradleUserHome: string) {
|
||||||
constructor(rootDir: string) {
|
super(gradleUserHome, 'project', 'Project configuration cache')
|
||||||
super('project', 'Project configuration cache')
|
|
||||||
this.rootDir = rootDir
|
|
||||||
}
|
|
||||||
|
|
||||||
protected cacheOutputExists(): boolean {
|
|
||||||
const dir = this.getProjectDotGradleDir()
|
|
||||||
return fs.existsSync(dir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getCachePath(): string[] {
|
protected getCachePath(): string[] {
|
||||||
const dir = this.getProjectDotGradleDir()
|
return this.getProjectRoots().map(x => path.resolve(x, '.gradle/configuration-cache'))
|
||||||
return PATHS_TO_CACHE.map(x => path.resolve(dir, x))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProjectDotGradleDir(): string {
|
protected initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void {
|
||||||
return path.resolve(this.rootDir, '.gradle')
|
const projectRootCapture = path.resolve(initScriptsDir, 'project-root-capture.init.gradle')
|
||||||
|
fs.writeFileSync(
|
||||||
|
projectRootCapture,
|
||||||
|
`
|
||||||
|
// Only run again root build. Do not run against included builds.
|
||||||
|
def isTopLevelBuild = gradle.getParent() == null
|
||||||
|
if (isTopLevelBuild) {
|
||||||
|
settingsEvaluated { settings ->
|
||||||
|
def projectRootEntry = settings.rootDir.absolutePath + "\\n"
|
||||||
|
def projectRootList = new File(settings.gradle.gradleUserHomeDir, "${META_FILE_DIR}/${PROJECT_ROOTS_FILE}")
|
||||||
|
println "Adding " + projectRootEntry + " to " + projectRootList
|
||||||
|
if (!projectRootList.exists() || !projectRootList.text.contains(projectRootEntry)) {
|
||||||
|
projectRootList << projectRootEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For every Gradle invocation, we record the project root directory. This method returns the entire
|
||||||
|
* set of project roots, to allow saving of configuration-cache entries for each.
|
||||||
|
*/
|
||||||
|
private getProjectRoots(): string[] {
|
||||||
|
const projectList = path.resolve(this.gradleUserHome, META_FILE_DIR, PROJECT_ROOTS_FILE)
|
||||||
|
if (!fs.existsSync(projectList)) {
|
||||||
|
core.info(`Missing project list file ${projectList}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const projectRoots = fs.readFileSync(projectList, 'utf-8')
|
||||||
|
core.info(`Found project roots '${projectRoots}' in ${projectList}`)
|
||||||
|
return projectRoots.trim().split('\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
123
src/cache-reporting.ts
Normal file
123
src/cache-reporting.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects information on what entries were saved and restored during the action.
|
||||||
|
* This information is used to generate a summary of the cache usage.
|
||||||
|
*/
|
||||||
|
export class CacheListener {
|
||||||
|
cacheEntries: CacheEntryListener[] = []
|
||||||
|
|
||||||
|
get fullyRestored(): boolean {
|
||||||
|
return this.cacheEntries.every(x => !x.wasRequestedButNotRestored())
|
||||||
|
}
|
||||||
|
|
||||||
|
entry(name: string): CacheEntryListener {
|
||||||
|
for (const entry of this.cacheEntries) {
|
||||||
|
if (entry.entryName === name) {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEntry = new CacheEntryListener(name)
|
||||||
|
this.cacheEntries.push(newEntry)
|
||||||
|
return newEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
stringify(): string {
|
||||||
|
return JSON.stringify(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
static rehydrate(stringRep: string): CacheListener {
|
||||||
|
const rehydrated: CacheListener = Object.assign(new CacheListener(), JSON.parse(stringRep))
|
||||||
|
const entries = rehydrated.cacheEntries
|
||||||
|
for (let index = 0; index < entries.length; index++) {
|
||||||
|
const rawEntry = entries[index]
|
||||||
|
entries[index] = Object.assign(new CacheEntryListener(rawEntry.entryName), rawEntry)
|
||||||
|
}
|
||||||
|
return rehydrated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects information on the state of a single cache entry.
|
||||||
|
*/
|
||||||
|
export class CacheEntryListener {
|
||||||
|
entryName: string
|
||||||
|
requestedKey: string | undefined
|
||||||
|
requestedRestoreKeys: string[] | undefined
|
||||||
|
restoredKey: string | undefined
|
||||||
|
restoredSize: number | undefined
|
||||||
|
|
||||||
|
savedKey: string | undefined
|
||||||
|
savedSize: number | undefined
|
||||||
|
|
||||||
|
constructor(entryName: string) {
|
||||||
|
this.entryName = entryName
|
||||||
|
}
|
||||||
|
|
||||||
|
wasRequestedButNotRestored(): boolean {
|
||||||
|
return this.requestedKey !== undefined && this.restoredKey === undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
markRequested(key: string, restoreKeys: string[] = []): CacheEntryListener {
|
||||||
|
this.requestedKey = key
|
||||||
|
this.requestedRestoreKeys = restoreKeys
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
markRestored(key: string, size: number | undefined): CacheEntryListener {
|
||||||
|
this.restoredKey = key
|
||||||
|
this.restoredSize = size
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
markSaved(key: string, size: number | undefined): CacheEntryListener {
|
||||||
|
this.savedKey = key
|
||||||
|
this.savedSize = size
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logCachingReport(listener: CacheListener): void {
|
||||||
|
if (listener.cacheEntries.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(`---------- Caching Summary -------------
|
||||||
|
Restored Entries Count: ${getCount(listener.cacheEntries, e => e.restoredSize)}
|
||||||
|
Size: ${getSum(listener.cacheEntries, e => e.restoredSize)}
|
||||||
|
Saved Entries Count: ${getCount(listener.cacheEntries, e => e.savedSize)}
|
||||||
|
Size: ${getSum(listener.cacheEntries, e => e.savedSize)}`)
|
||||||
|
|
||||||
|
core.startGroup('Cache Entry details')
|
||||||
|
for (const entry of listener.cacheEntries) {
|
||||||
|
core.info(`Entry: ${entry.entryName}
|
||||||
|
Requested Key : ${entry.requestedKey ?? ''}
|
||||||
|
Restored Key : ${entry.restoredKey ?? ''}
|
||||||
|
Size: ${formatSize(entry.restoredSize)}
|
||||||
|
Saved Key : ${entry.savedKey ?? ''}
|
||||||
|
Size: ${formatSize(entry.savedSize)}`)
|
||||||
|
}
|
||||||
|
core.endGroup()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCount(
|
||||||
|
cacheEntries: CacheEntryListener[],
|
||||||
|
predicate: (value: CacheEntryListener) => number | undefined
|
||||||
|
): number {
|
||||||
|
return cacheEntries.filter(e => predicate(e) !== undefined).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSum(
|
||||||
|
cacheEntries: CacheEntryListener[],
|
||||||
|
predicate: (value: CacheEntryListener) => number | undefined
|
||||||
|
): string {
|
||||||
|
return formatSize(cacheEntries.map(e => predicate(e) ?? 0).reduce((p, v) => p + v, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes: number | undefined): string {
|
||||||
|
if (bytes === undefined || bytes === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return `${Math.round(bytes / (1024 * 1024))} MB (${bytes} B)`
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import * as crypto from 'crypto'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
|
||||||
|
const JOB_CONTEXT_PARAMETER = 'workflow-job-context'
|
||||||
const CACHE_DISABLED_PARAMETER = 'cache-disabled'
|
const CACHE_DISABLED_PARAMETER = 'cache-disabled'
|
||||||
const CACHE_READONLY_PARAMETER = 'cache-read-only'
|
const CACHE_READONLY_PARAMETER = 'cache-read-only'
|
||||||
const CACHE_DEBUG_VAR = 'GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED'
|
const CACHE_DEBUG_VAR = 'GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED'
|
||||||
|
@ -26,6 +27,17 @@ export function getCacheKeyPrefix(): string {
|
||||||
return process.env[CACHE_PREFIX_VAR] || ''
|
return process.env[CACHE_PREFIX_VAR] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function determineJobContext(): string {
|
||||||
|
// By default, we hash the full `matrix` data for the run, to uniquely identify this job invocation
|
||||||
|
// The only way we can obtain the `matrix` data is via the `workflow-job-context` parameter in action.yml.
|
||||||
|
const workflowJobContext = core.getInput(JOB_CONTEXT_PARAMETER)
|
||||||
|
return hashStrings([workflowJobContext])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashFileNames(fileNames: string[]): string {
|
||||||
|
return hashStrings(fileNames.map(x => x.replace(new RegExp(`\\${path.sep}`, 'g'), '/')))
|
||||||
|
}
|
||||||
|
|
||||||
export function hashStrings(values: string[]): string {
|
export function hashStrings(values: string[]): string {
|
||||||
const hash = crypto.createHash('md5')
|
const hash = crypto.createHash('md5')
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
|
@ -34,10 +46,6 @@ export function hashStrings(values: string[]): string {
|
||||||
return hash.digest('hex')
|
return hash.digest('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hashFileNames(fileNames: string[]): string {
|
|
||||||
return hashStrings(fileNames.map(x => x.replace(new RegExp(`\\${path.sep}`, 'g'), '/')))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleCacheFailure(error: unknown, message: string): void {
|
export function handleCacheFailure(error: unknown, message: string): void {
|
||||||
if (error instanceof cache.ValidationError) {
|
if (error instanceof cache.ValidationError) {
|
||||||
// Fail on cache validation errors
|
// Fail on cache validation errors
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
import {GradleUserHomeCache} from './cache-gradle-user-home'
|
import {GradleUserHomeCache} from './cache-gradle-user-home'
|
||||||
import {ProjectDotGradleCache} from './cache-project-dot-gradle'
|
import {ProjectDotGradleCache} from './cache-project-dot-gradle'
|
||||||
import * as core from '@actions/core'
|
|
||||||
import {isCacheDisabled, isCacheReadOnly} from './cache-utils'
|
import {isCacheDisabled, isCacheReadOnly} from './cache-utils'
|
||||||
import {CacheEntryListener, CacheListener} from './cache-base'
|
import {logCachingReport, CacheListener} from './cache-reporting'
|
||||||
|
|
||||||
const BUILD_ROOT_DIR = 'BUILD_ROOT_DIR'
|
const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED'
|
||||||
|
const GRADLE_USER_HOME = 'GRADLE_USER_HOME'
|
||||||
const CACHE_LISTENER = 'CACHE_LISTENER'
|
const CACHE_LISTENER = 'CACHE_LISTENER'
|
||||||
|
|
||||||
export async function restore(buildRootDirectory: string): Promise<void> {
|
export async function restore(gradleUserHome: string): Promise<void> {
|
||||||
const gradleUserHomeCache = new GradleUserHomeCache(buildRootDirectory)
|
if (!shouldRestoreCaches()) {
|
||||||
const projectDotGradleCache = new ProjectDotGradleCache(buildRootDirectory)
|
|
||||||
|
|
||||||
gradleUserHomeCache.init()
|
|
||||||
|
|
||||||
if (isCacheDisabled()) {
|
|
||||||
core.info('Cache is disabled: will not restore state from previous builds.')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gradleUserHomeCache = new GradleUserHomeCache(gradleUserHome)
|
||||||
|
gradleUserHomeCache.init()
|
||||||
|
|
||||||
|
const projectDotGradleCache = new ProjectDotGradleCache(gradleUserHome)
|
||||||
|
projectDotGradleCache.init()
|
||||||
|
|
||||||
await core.group('Restore Gradle state from cache', async () => {
|
await core.group('Restore Gradle state from cache', async () => {
|
||||||
core.saveState(BUILD_ROOT_DIR, buildRootDirectory)
|
core.saveState(GRADLE_USER_HOME, gradleUserHome)
|
||||||
|
|
||||||
const cacheListener = new CacheListener()
|
const cacheListener = new CacheListener()
|
||||||
await gradleUserHomeCache.restore(cacheListener)
|
await gradleUserHomeCache.restore(cacheListener)
|
||||||
|
@ -38,6 +39,10 @@ export async function restore(buildRootDirectory: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(): Promise<void> {
|
export async function save(): Promise<void> {
|
||||||
|
if (!shouldSaveCaches()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER))
|
const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER))
|
||||||
|
|
||||||
if (isCacheReadOnly()) {
|
if (isCacheReadOnly()) {
|
||||||
|
@ -47,56 +52,44 @@ export async function save(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
await core.group('Caching Gradle state', async () => {
|
await core.group('Caching Gradle state', async () => {
|
||||||
const buildRootDirectory = core.getState(BUILD_ROOT_DIR)
|
const gradleUserHome = core.getState(GRADLE_USER_HOME)
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
new GradleUserHomeCache(buildRootDirectory).save(cacheListener),
|
new GradleUserHomeCache(gradleUserHome).save(cacheListener),
|
||||||
new ProjectDotGradleCache(buildRootDirectory).save(cacheListener)
|
new ProjectDotGradleCache(gradleUserHome).save(cacheListener)
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
logCachingReport(cacheListener)
|
logCachingReport(cacheListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
function logCachingReport(listener: CacheListener): void {
|
function shouldRestoreCaches(): boolean {
|
||||||
if (listener.cacheEntries.length === 0) {
|
if (isCacheDisabled()) {
|
||||||
return
|
core.info('Cache is disabled: will not restore state from previous builds.')
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info(`---------- Caching Summary -------------
|
if (process.env[CACHE_RESTORED_VAR]) {
|
||||||
Restored Entries Count: ${getCount(listener.cacheEntries, e => e.restoredSize)}
|
core.info('Cache only restored on first action step.')
|
||||||
Size: ${getSum(listener.cacheEntries, e => e.restoredSize)}
|
return false
|
||||||
Saved Entries Count: ${getCount(listener.cacheEntries, e => e.savedSize)}
|
|
||||||
Size: ${getSum(listener.cacheEntries, e => e.savedSize)}`)
|
|
||||||
|
|
||||||
core.startGroup('Cache Entry details')
|
|
||||||
for (const entry of listener.cacheEntries) {
|
|
||||||
core.info(`Entry: ${entry.entryName}
|
|
||||||
Requested Key : ${entry.requestedKey ?? ''}
|
|
||||||
Restored Key : ${entry.restoredKey ?? ''}
|
|
||||||
Size: ${formatSize(entry.restoredSize)}
|
|
||||||
Saved Key : ${entry.savedKey ?? ''}
|
|
||||||
Size: ${formatSize(entry.savedSize)}`)
|
|
||||||
}
|
}
|
||||||
core.endGroup()
|
|
||||||
|
// Export var that is detected in all later restore steps
|
||||||
|
core.exportVariable(CACHE_RESTORED_VAR, true)
|
||||||
|
// Export state that is detected in corresponding post-action step
|
||||||
|
core.saveState(CACHE_RESTORED_VAR, true)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCount(
|
function shouldSaveCaches(): boolean {
|
||||||
cacheEntries: CacheEntryListener[],
|
if (isCacheDisabled()) {
|
||||||
predicate: (value: CacheEntryListener) => number | undefined
|
core.info('Cache is disabled: will not save state for later builds.')
|
||||||
): number {
|
return false
|
||||||
return cacheEntries.filter(e => predicate(e) !== undefined).length
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSum(
|
|
||||||
cacheEntries: CacheEntryListener[],
|
|
||||||
predicate: (value: CacheEntryListener) => number | undefined
|
|
||||||
): string {
|
|
||||||
return formatSize(cacheEntries.map(e => predicate(e) ?? 0).reduce((p, v) => p + v, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSize(bytes: number | undefined): string {
|
|
||||||
if (bytes === undefined || bytes === 0) {
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
return `${Math.round(bytes / (1024 * 1024))} MB (${bytes} B)`
|
|
||||||
|
if (!core.getState(CACHE_RESTORED_VAR)) {
|
||||||
|
core.info('Cache will only be saved in final post-action step.')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
13
src/main.ts
13
src/main.ts
|
@ -1,5 +1,6 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import * as os from 'os'
|
||||||
import {parseArgsStringToArgv} from 'string-argv'
|
import {parseArgsStringToArgv} from 'string-argv'
|
||||||
|
|
||||||
import * as caches from './caches'
|
import * as caches from './caches'
|
||||||
|
@ -14,8 +15,9 @@ export async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const workspaceDirectory = process.env[`GITHUB_WORKSPACE`] || ''
|
const workspaceDirectory = process.env[`GITHUB_WORKSPACE`] || ''
|
||||||
const buildRootDirectory = resolveBuildRootDirectory(workspaceDirectory)
|
const buildRootDirectory = resolveBuildRootDirectory(workspaceDirectory)
|
||||||
|
const gradleUserHome = determineGradleUserHome(buildRootDirectory)
|
||||||
|
|
||||||
await caches.restore(buildRootDirectory)
|
await caches.restore(gradleUserHome)
|
||||||
|
|
||||||
const args: string[] = parseCommandLineArguments()
|
const args: string[] = parseCommandLineArguments()
|
||||||
|
|
||||||
|
@ -63,6 +65,15 @@ function resolveBuildRootDirectory(baseDirectory: string): string {
|
||||||
return resolvedBuildRootDirectory
|
return resolvedBuildRootDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function determineGradleUserHome(rootDir: string): string {
|
||||||
|
const customGradleUserHome = process.env['GRADLE_USER_HOME']
|
||||||
|
if (customGradleUserHome) {
|
||||||
|
return path.resolve(rootDir, customGradleUserHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(os.homedir(), '.gradle')
|
||||||
|
}
|
||||||
|
|
||||||
function parseCommandLineArguments(): string[] {
|
function parseCommandLineArguments(): string[] {
|
||||||
const input = core.getInput('arguments')
|
const input = core.getInput('arguments')
|
||||||
return parseArgsStringToArgv(input)
|
return parseArgsStringToArgv(input)
|
||||||
|
|
Loading…
Reference in a new issue