mirror of
https://github.com/gradle/actions
synced 2024-11-27 11:52:24 +00:00
Cache validated checksums for later executions
The most common case for validation will be that the wrapper jars are unchanged from a previous workflow run. In this case, we cache the validated wrapper checksums to minimise the work required on a subsequent run. Fixes #172
This commit is contained in:
parent
ce4c3a6c5e
commit
b6395da67c
8 changed files with 98 additions and 13 deletions
|
@ -4,12 +4,11 @@ import * as core from '@actions/core'
|
||||||
import * as glob from '@actions/glob'
|
import * as glob from '@actions/glob'
|
||||||
import * as semver from 'semver'
|
import * as semver from 'semver'
|
||||||
|
|
||||||
import {META_FILE_DIR} from './gradle-user-home-cache'
|
|
||||||
import {CacheEntryListener, CacheListener} from './cache-reporting'
|
import {CacheEntryListener, CacheListener} from './cache-reporting'
|
||||||
import {cacheDebug, hashFileNames, isCacheDebuggingEnabled, restoreCache, saveCache, tryDelete} from './cache-utils'
|
import {cacheDebug, hashFileNames, isCacheDebuggingEnabled, restoreCache, saveCache, tryDelete} from './cache-utils'
|
||||||
|
|
||||||
import {BuildResult, loadBuildResults} from '../build-results'
|
import {BuildResult, loadBuildResults} from '../build-results'
|
||||||
import {CacheConfig} from '../configuration'
|
import {CacheConfig, ACTION_METADATA_DIR} from '../configuration'
|
||||||
import {getCacheKeyBase} from './cache-key'
|
import {getCacheKeyBase} from './cache-key'
|
||||||
|
|
||||||
const SKIP_RESTORE_VAR = 'GRADLE_BUILD_ACTION_SKIP_RESTORE'
|
const SKIP_RESTORE_VAR = 'GRADLE_BUILD_ACTION_SKIP_RESTORE'
|
||||||
|
@ -298,7 +297,7 @@ abstract class AbstractEntryExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCacheMetadataFile(): string {
|
private getCacheMetadataFile(): string {
|
||||||
const actionMetadataDirectory = path.resolve(this.gradleUserHome, META_FILE_DIR)
|
const actionMetadataDirectory = path.resolve(this.gradleUserHome, ACTION_METADATA_DIR)
|
||||||
fs.mkdirSync(actionMetadataDirectory, {recursive: true})
|
fs.mkdirSync(actionMetadataDirectory, {recursive: true})
|
||||||
|
|
||||||
return path.resolve(actionMetadataDirectory, `${this.extractorName}-entry-metadata.json`)
|
return path.resolve(actionMetadataDirectory, `${this.extractorName}-entry-metadata.json`)
|
||||||
|
|
|
@ -7,14 +7,12 @@ import fs from 'fs'
|
||||||
import {generateCacheKey} from './cache-key'
|
import {generateCacheKey} from './cache-key'
|
||||||
import {CacheListener} from './cache-reporting'
|
import {CacheListener} from './cache-reporting'
|
||||||
import {saveCache, restoreCache, cacheDebug, isCacheDebuggingEnabled, tryDelete} from './cache-utils'
|
import {saveCache, restoreCache, cacheDebug, isCacheDebuggingEnabled, tryDelete} from './cache-utils'
|
||||||
import {CacheConfig} from '../configuration'
|
import {CacheConfig, ACTION_METADATA_DIR} from '../configuration'
|
||||||
import {GradleHomeEntryExtractor, ConfigurationCacheEntryExtractor} from './gradle-home-extry-extractor'
|
import {GradleHomeEntryExtractor, ConfigurationCacheEntryExtractor} from './gradle-home-extry-extractor'
|
||||||
import {getPredefinedToolchains, mergeToolchainContent, readResourceFileAsString} from './gradle-user-home-utils'
|
import {getPredefinedToolchains, mergeToolchainContent, readResourceFileAsString} from './gradle-user-home-utils'
|
||||||
|
|
||||||
const RESTORED_CACHE_KEY_KEY = 'restored-cache-key'
|
const RESTORED_CACHE_KEY_KEY = 'restored-cache-key'
|
||||||
|
|
||||||
export const META_FILE_DIR = '.setup-gradle'
|
|
||||||
|
|
||||||
export class GradleUserHomeCache {
|
export class GradleUserHomeCache {
|
||||||
private readonly cacheName = 'home'
|
private readonly cacheName = 'home'
|
||||||
private readonly cacheDescription = 'Gradle User Home'
|
private readonly cacheDescription = 'Gradle User Home'
|
||||||
|
@ -172,7 +170,7 @@ export class GradleUserHomeCache {
|
||||||
*/
|
*/
|
||||||
protected getCachePath(): string[] {
|
protected getCachePath(): string[] {
|
||||||
const rawPaths: string[] = this.cacheConfig.getCacheIncludes()
|
const rawPaths: string[] = this.cacheConfig.getCacheIncludes()
|
||||||
rawPaths.push(META_FILE_DIR)
|
rawPaths.push(ACTION_METADATA_DIR)
|
||||||
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
|
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
|
||||||
cacheDebug(`Using cache paths: ${resolvedPaths}`)
|
cacheDebug(`Using cache paths: ${resolvedPaths}`)
|
||||||
return resolvedPaths
|
return resolvedPaths
|
||||||
|
@ -188,7 +186,7 @@ export class GradleUserHomeCache {
|
||||||
|
|
||||||
private initializeGradleUserHome(): void {
|
private initializeGradleUserHome(): void {
|
||||||
// Create a directory for storing action metadata
|
// Create a directory for storing action metadata
|
||||||
const actionCacheDir = path.resolve(this.gradleUserHome, META_FILE_DIR)
|
const actionCacheDir = path.resolve(this.gradleUserHome, ACTION_METADATA_DIR)
|
||||||
fs.mkdirSync(actionCacheDir, {recursive: true})
|
fs.mkdirSync(actionCacheDir, {recursive: true})
|
||||||
|
|
||||||
this.copyInitScripts()
|
this.copyInitScripts()
|
||||||
|
|
|
@ -8,6 +8,8 @@ import path from 'path'
|
||||||
|
|
||||||
const ACTION_ID_VAR = 'GRADLE_ACTION_ID'
|
const ACTION_ID_VAR = 'GRADLE_ACTION_ID'
|
||||||
|
|
||||||
|
export const ACTION_METADATA_DIR = '.setup-gradle'
|
||||||
|
|
||||||
export class DependencyGraphConfig {
|
export class DependencyGraphConfig {
|
||||||
getDependencyGraphOption(): DependencyGraphOption {
|
getDependencyGraphOption(): DependencyGraphOption {
|
||||||
const val = core.getInput('dependency-graph')
|
const val = core.getInput('dependency-graph')
|
||||||
|
|
|
@ -46,13 +46,13 @@ export async function setup(
|
||||||
core.saveState(USER_HOME, userHome)
|
core.saveState(USER_HOME, userHome)
|
||||||
core.saveState(GRADLE_USER_HOME, gradleUserHome)
|
core.saveState(GRADLE_USER_HOME, gradleUserHome)
|
||||||
|
|
||||||
await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory())
|
|
||||||
|
|
||||||
const cacheListener = new CacheListener()
|
const cacheListener = new CacheListener()
|
||||||
await caches.restore(userHome, gradleUserHome, cacheListener, cacheConfig)
|
await caches.restore(userHome, gradleUserHome, cacheListener, cacheConfig)
|
||||||
|
|
||||||
core.saveState(CACHE_LISTENER, cacheListener.stringify())
|
core.saveState(CACHE_LISTENER, cacheListener.stringify())
|
||||||
|
|
||||||
|
await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome)
|
||||||
|
|
||||||
await buildScan.setup(buildScanConfig)
|
await buildScan.setup(buildScanConfig)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
26
sources/src/wrapper-validation/cache.ts
Normal file
26
sources/src/wrapper-validation/cache.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import {ACTION_METADATA_DIR} from '../configuration'
|
||||||
|
|
||||||
|
export class ChecksumCache {
|
||||||
|
private readonly cacheFile: string
|
||||||
|
|
||||||
|
constructor(gradleUserHome: string) {
|
||||||
|
this.cacheFile = path.resolve(gradleUserHome, ACTION_METADATA_DIR, 'valid-wrappers.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): string[] {
|
||||||
|
// Load previously validated checksums saved in Gradle User Home
|
||||||
|
if (fs.existsSync(this.cacheFile)) {
|
||||||
|
return JSON.parse(fs.readFileSync(this.cacheFile, 'utf-8'))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
save(checksums: string[]): void {
|
||||||
|
const uniqueChecksums = [...new Set(checksums)]
|
||||||
|
// Save validated checksums to Gradle User Home
|
||||||
|
fs.mkdirSync(path.dirname(this.cacheFile), {recursive: true})
|
||||||
|
fs.writeFileSync(this.cacheFile, JSON.stringify(uniqueChecksums))
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ export async function findInvalidWrapperJars(
|
||||||
minWrapperCount: number,
|
minWrapperCount: number,
|
||||||
allowSnapshots: boolean,
|
allowSnapshots: boolean,
|
||||||
allowedChecksums: string[],
|
allowedChecksums: string[],
|
||||||
|
previouslyValidatedChecksums: string[] = [],
|
||||||
knownValidChecksums: checksums.WrapperChecksums = checksums.KNOWN_CHECKSUMS
|
knownValidChecksums: checksums.WrapperChecksums = checksums.KNOWN_CHECKSUMS
|
||||||
): Promise<ValidationResult> {
|
): Promise<ValidationResult> {
|
||||||
const wrapperJars = await find.findWrapperJars(gitRepoRoot)
|
const wrapperJars = await find.findWrapperJars(gitRepoRoot)
|
||||||
|
@ -21,7 +22,11 @@ export async function findInvalidWrapperJars(
|
||||||
const notYetValidatedWrappers = []
|
const notYetValidatedWrappers = []
|
||||||
for (const wrapperJar of wrapperJars) {
|
for (const wrapperJar of wrapperJars) {
|
||||||
const sha = await hash.sha256File(resolve(gitRepoRoot, wrapperJar))
|
const sha = await hash.sha256File(resolve(gitRepoRoot, wrapperJar))
|
||||||
if (allowedChecksums.includes(sha) || knownValidChecksums.checksums.has(sha)) {
|
if (
|
||||||
|
allowedChecksums.includes(sha) ||
|
||||||
|
previouslyValidatedChecksums.includes(sha) ||
|
||||||
|
knownValidChecksums.checksums.has(sha)
|
||||||
|
) {
|
||||||
result.valid.push(new WrapperJar(wrapperJar, sha))
|
result.valid.push(new WrapperJar(wrapperJar, sha))
|
||||||
} else {
|
} else {
|
||||||
notYetValidatedWrappers.push(new WrapperJar(wrapperJar, sha))
|
notYetValidatedWrappers.push(new WrapperJar(wrapperJar, sha))
|
||||||
|
|
|
@ -1,17 +1,33 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
import {WrapperValidationConfig} from '../configuration'
|
import {WrapperValidationConfig} from '../configuration'
|
||||||
|
import {ChecksumCache} from './cache'
|
||||||
import {findInvalidWrapperJars} from './validate'
|
import {findInvalidWrapperJars} from './validate'
|
||||||
import {JobFailure} from '../errors'
|
import {JobFailure} from '../errors'
|
||||||
|
|
||||||
export async function validateWrappers(config: WrapperValidationConfig, workspaceRoot: string): Promise<void> {
|
export async function validateWrappers(
|
||||||
|
config: WrapperValidationConfig,
|
||||||
|
workspaceRoot: string,
|
||||||
|
gradleUserHome: string
|
||||||
|
): Promise<void> {
|
||||||
if (!config.doValidateWrappers()) {
|
if (!config.doValidateWrappers()) {
|
||||||
return // Wrapper validation is disabled
|
return // Wrapper validation is disabled
|
||||||
}
|
}
|
||||||
|
const checksumCache = new ChecksumCache(gradleUserHome)
|
||||||
|
|
||||||
const allowedChecksums = process.env['ALLOWED_GRADLE_WRAPPER_CHECKSUMS']?.split(',') || []
|
const allowedChecksums = process.env['ALLOWED_GRADLE_WRAPPER_CHECKSUMS']?.split(',') || []
|
||||||
const result = await findInvalidWrapperJars(workspaceRoot, 0, config.allowSnapshotWrappers(), allowedChecksums)
|
const previouslyValidatedChecksums = checksumCache.load()
|
||||||
|
|
||||||
|
const result = await findInvalidWrapperJars(
|
||||||
|
workspaceRoot,
|
||||||
|
0,
|
||||||
|
config.allowSnapshotWrappers(),
|
||||||
|
allowedChecksums,
|
||||||
|
previouslyValidatedChecksums
|
||||||
|
)
|
||||||
if (result.isValid()) {
|
if (result.isValid()) {
|
||||||
await core.group('All Gradle Wrapper jars are valid', async () => {
|
await core.group('All Gradle Wrapper jars are valid', async () => {
|
||||||
|
core.info(`Loaded previously validated checksums from cache: ${previouslyValidatedChecksums.join(', ')}`)
|
||||||
core.info(result.toDisplayString())
|
core.info(result.toDisplayString())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -20,4 +36,6 @@ export async function validateWrappers(config: WrapperValidationConfig, workspac
|
||||||
`Gradle Wrapper Validation Failed!\n See https://github.com/gradle/actions/blob/main/docs/wrapper-validation.md#reporting-failures\n${result.toDisplayString()}`
|
`Gradle Wrapper Validation Failed!\n See https://github.com/gradle/actions/blob/main/docs/wrapper-validation.md#reporting-failures\n${result.toDisplayString()}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checksumCache.save(result.valid.map(wrapper => wrapper.checksum))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
import * as validate from '../../../src/wrapper-validation/validate'
|
import * as validate from '../../../src/wrapper-validation/validate'
|
||||||
import {expect, test, jest} from '@jest/globals'
|
import {expect, test, jest} from '@jest/globals'
|
||||||
import { WrapperChecksums } from '../../../src/wrapper-validation/checksums'
|
import { WrapperChecksums } from '../../../src/wrapper-validation/checksums'
|
||||||
|
import { ChecksumCache } from '../../../src/wrapper-validation/cache'
|
||||||
|
import exp from 'constants'
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
const baseDir = path.resolve('./test/jest/wrapper-validation')
|
const baseDir = path.resolve('./test/jest/wrapper-validation')
|
||||||
|
const tmpDir = path.resolve('./test/jest/tmp')
|
||||||
|
|
||||||
test('succeeds if all found wrapper jars are valid', async () => {
|
test('succeeds if all found wrapper jars are valid', async () => {
|
||||||
const result = await validate.findInvalidWrapperJars(baseDir, 3, false, [
|
const result = await validate.findInvalidWrapperJars(baseDir, 3, false, [
|
||||||
|
@ -24,6 +28,24 @@ test('succeeds if all found wrapper jars are valid', async () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('succeeds if all found wrapper jars are previously valid', async () => {
|
||||||
|
const result = await validate.findInvalidWrapperJars(baseDir, 3, false, [], [
|
||||||
|
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
||||||
|
'3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce'
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(result.isValid()).toBe(true)
|
||||||
|
// Only hardcoded and explicitly allowed checksums should have been used
|
||||||
|
expect(result.fetchedChecksums).toBe(false)
|
||||||
|
|
||||||
|
expect(result.toDisplayString()).toBe(
|
||||||
|
'✓ Found known Gradle Wrapper JAR files:\n' +
|
||||||
|
' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradle-wrapper.jar\n' +
|
||||||
|
' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradlе-wrapper.jar\n' + // homoglyph
|
||||||
|
' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('succeeds if all found wrapper jars are valid (and checksums are fetched from Gradle API)', async () => {
|
test('succeeds if all found wrapper jars are valid (and checksums are fetched from Gradle API)', async () => {
|
||||||
const knownValidChecksums = new WrapperChecksums()
|
const knownValidChecksums = new WrapperChecksums()
|
||||||
const result = await validate.findInvalidWrapperJars(
|
const result = await validate.findInvalidWrapperJars(
|
||||||
|
@ -31,6 +53,7 @@ test('succeeds if all found wrapper jars are valid (and checksums are fetched fr
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
['e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'],
|
['e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'],
|
||||||
|
[],
|
||||||
knownValidChecksums
|
knownValidChecksums
|
||||||
)
|
)
|
||||||
console.log(`fetchedChecksums = ${result.fetchedChecksums}`)
|
console.log(`fetchedChecksums = ${result.fetchedChecksums}`)
|
||||||
|
@ -98,3 +121,17 @@ test('fails if not enough wrapper jars are found', async () => {
|
||||||
' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar'
|
' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('can save and load checksums', async () => {
|
||||||
|
const cacheDir = path.join(tmpDir, 'wrapper-validation-cache')
|
||||||
|
fs.rmSync(cacheDir, {recursive: true, force: true})
|
||||||
|
|
||||||
|
const checksumCache = new ChecksumCache(cacheDir)
|
||||||
|
|
||||||
|
expect(checksumCache.load()).toEqual([])
|
||||||
|
|
||||||
|
checksumCache.save(['123', '456'])
|
||||||
|
|
||||||
|
expect(checksumCache.load()).toEqual(['123', '456'])
|
||||||
|
expect(fs.existsSync(cacheDir)).toBe(true)
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in a new issue