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:
daz 2024-08-01 09:39:30 -06:00
parent ce4c3a6c5e
commit b6395da67c
No known key found for this signature in database
8 changed files with 98 additions and 13 deletions

View file

@ -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`)

View file

@ -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()

View file

@ -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')

View file

@ -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

View 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))
}
}

View file

@ -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))

View file

@ -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))
} }

View file

@ -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)
})