Isolate uses of GitHub cache API

- Introduced cache-api with Cache interfaces
- Extracted github-actions-cache implementation
- Use the cache-api consistently for all cache access
This commit is contained in:
daz 2025-01-30 14:23:30 -07:00
parent 44db8041a5
commit e03531acf1
No known key found for this signature in database
6 changed files with 120 additions and 41 deletions

View file

@ -0,0 +1,37 @@
import {GitHubActionsCache} from './github-actions-cache'
export function getCache(): Cache {
return new GitHubActionsCache()
}
export interface Cache {
/**
* @returns boolean return true if cache service feature is available, otherwise false
*/
isAvailable(): boolean
/**
* Restores cache from keys
*
* @param paths a list of file paths to restore from the cache
* @param primaryKey an explicit key for restoring the cache. Lookup is done with prefix matching.
* @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for primaryKey
* @returns the restored entry details for the cache hit, otherwise returns undefined
*/
restore(paths: string[], primaryKey: string, restoreKeys?: string[]): Promise<CacheResult | undefined>
/**
* Saves a list of files with the specified key
*
* @param paths a list of file paths to be cached
* @param key an explicit key for restoring the cache
* @returns the saved entry details if the cache was saved successfully and throws an error if save fails
*/
save(paths: string[], key: string): Promise<CacheResult>
}
export declare class CacheResult {
key: string
size?: number
constructor(key: string, size?: number)
}

View file

@ -1,4 +1,5 @@
import * as cache from '@actions/cache'
import * as core from '@actions/core'
import {getCache} from './cache-api'
export const DEFAULT_CACHE_ENABLED_REASON = `[Cache was enabled](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#caching-build-state-between-jobs). Action attempted to both restore and save the Gradle User Home.`
@ -28,6 +29,7 @@ export const CLEANUP_DISABLED_DUE_TO_CONFIG_CACHE_HIT =
*/
export class CacheListener {
cacheEntries: CacheEntryListener[] = []
cacheAvailable = getCache().isAvailable()
cacheReadOnly = false
cacheWriteOnly = false
cacheDisabled = false
@ -39,7 +41,7 @@ export class CacheListener {
}
get cacheStatus(): string {
if (!cache.isFeatureAvailable()) return 'not available'
if (!this.cacheAvailable) return 'not available'
if (this.cacheDisabled) return 'disabled'
if (this.cacheWriteOnly) return 'write-only'
if (this.cacheReadOnly) return 'read-only'
@ -133,6 +135,8 @@ export class CacheEntryListener {
}
markRestored(key: string, size: number | undefined, time: number): CacheEntryListener {
core.info(`Restored cache entry with key ${key} in ${time}ms`)
this.restoredKey = key
this.restoredSize = size
this.restoredTime = time
@ -145,6 +149,8 @@ export class CacheEntryListener {
}
markSaved(key: string, size: number | undefined, time: number): CacheEntryListener {
core.info(`Saved cache entry with key ${key} in ${time}ms`)
this.savedKey = key
this.savedSize = size
this.savedTime = time

View file

@ -1,5 +1,4 @@
import * as core from '@actions/core'
import * as cache from '@actions/cache'
import * as exec from '@actions/exec'
import * as crypto from 'crypto'
@ -7,9 +6,7 @@ import * as path from 'path'
import * as fs from 'fs'
import {CacheEntryListener} from './cache-reporting'
const SEGMENT_DOWNLOAD_TIMEOUT_VAR = 'SEGMENT_DOWNLOAD_TIMEOUT_MINS'
const SEGMENT_DOWNLOAD_TIMEOUT_DEFAULT = 10 * 60 * 1000 // 10 minutes
import {CacheResult, getCache} from './cache-api'
export function isCacheDebuggingEnabled(): boolean {
if (core.isDebug()) {
@ -31,23 +28,18 @@ export function hashStrings(values: string[]): string {
}
export async function restoreCache(
cachePath: string[],
cachePaths: string[],
cacheKey: string,
cacheRestoreKeys: string[],
listener: CacheEntryListener
): Promise<cache.CacheEntry | undefined> {
): Promise<CacheResult | undefined> {
listener.markRequested(cacheKey, cacheRestoreKeys)
try {
const startTime = Date.now()
// Only override the read timeout if the SEGMENT_DOWNLOAD_TIMEOUT_MINS env var has NOT been set
const cacheRestoreOptions = process.env[SEGMENT_DOWNLOAD_TIMEOUT_VAR]
? {}
: {segmentTimeoutInMs: SEGMENT_DOWNLOAD_TIMEOUT_DEFAULT}
const restoredEntry = await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys, cacheRestoreOptions)
const restoredEntry = await getCache().restore(cachePaths, cacheKey, cacheRestoreKeys)
if (restoredEntry !== undefined) {
const restoreTime = Date.now() - startTime
listener.markRestored(restoredEntry.key, restoredEntry.size, restoreTime)
core.info(`Restored cache entry with key ${cacheKey} to ${cachePath.join()} in ${restoreTime}ms`)
}
return restoredEntry
} catch (error) {
@ -57,20 +49,19 @@ export async function restoreCache(
}
}
export async function saveCache(cachePath: string[], cacheKey: string, listener: CacheEntryListener): Promise<void> {
export async function saveCache(cachePaths: string[], cacheKey: string, listener: CacheEntryListener): Promise<void> {
try {
const startTime = Date.now()
const savedEntry = await cache.saveCache(cachePath, cacheKey)
const saveResult = await getCache().save(cachePaths, cacheKey)
const saveTime = Date.now() - startTime
listener.markSaved(savedEntry.key, savedEntry.size, saveTime)
core.info(`Saved cache entry with key ${cacheKey} from ${cachePath.join()} in ${saveTime}ms`)
} catch (error) {
if (error instanceof cache.ReserveCacheError) {
if (saveResult.size === 0) {
listener.markAlreadyExists(cacheKey)
} else {
listener.markNotSaved((error as Error).message)
listener.markSaved(saveResult.key, saveResult.size, saveTime)
}
handleCacheFailure(error, `Failed to save cache entry with path '${cachePath}' and key: ${cacheKey}`)
} catch (error) {
listener.markNotSaved((error as Error).message)
handleCacheFailure(error, `Failed to save cache entry with path '${cachePaths}' and key: ${cacheKey}`)
}
}
@ -83,19 +74,10 @@ export function cacheDebug(message: string): void {
}
export function handleCacheFailure(error: unknown, message: string): void {
if (error instanceof cache.ValidationError) {
// Fail on cache validation errors
throw error
}
if (error instanceof cache.ReserveCacheError) {
// Reserve cache errors are expected if the artifact has been previously cached
core.info(`${message}: ${error}`)
} else {
// Warn on all other errors
core.warning(`${message}: ${error}`)
if (error instanceof Error && error.stack) {
cacheDebug(error.stack)
}
// Warn on and continue on any cache error
core.warning(`${message}: ${error}`)
if (error instanceof Error && error.stack) {
cacheDebug(error.stack)
}
}

View file

@ -0,0 +1,49 @@
import * as gitHubCache from '@actions/cache'
import * as core from '@actions/core'
import {Cache} from './cache-api'
const SEGMENT_DOWNLOAD_TIMEOUT_VAR = 'SEGMENT_DOWNLOAD_TIMEOUT_MINS'
const SEGMENT_DOWNLOAD_TIMEOUT_DEFAULT = 10 * 60 * 1000 // 10 minutes
export class GitHubActionsCache implements Cache {
isAvailable(): boolean {
return gitHubCache.isFeatureAvailable()
}
async restore(paths: string[], primaryKey: string, restoreKeys?: string[]): Promise<CacheResult | undefined> {
// Only override the read timeout if the SEGMENT_DOWNLOAD_TIMEOUT_MINS env var has NOT been set
const cacheRestoreOptions = process.env[SEGMENT_DOWNLOAD_TIMEOUT_VAR]
? {}
: {segmentTimeoutInMs: SEGMENT_DOWNLOAD_TIMEOUT_DEFAULT}
const restored = await gitHubCache.restoreCache(paths, primaryKey, restoreKeys, cacheRestoreOptions)
return restored ? this.cacheResult(restored.key, restored.size) : undefined
}
async save(paths: string[], key: string): Promise<CacheResult> {
try {
const cacheEntry = await gitHubCache.saveCache(paths, key)
return this.cacheResult(cacheEntry.key, cacheEntry.size)
} catch (error) {
if (error instanceof gitHubCache.ReserveCacheError) {
// Reserve cache errors are expected if the artifact has been previously cached
core.info(`Cache entry ${key} already exists: ${error}`)
return this.cacheResult(key, 0)
}
throw error
}
}
private cacheResult(key: string, size?: number): CacheResult {
return new CacheResult(key, size)
}
}
class CacheResult {
key: string
size?: number
constructor(key: string, size?: number) {
this.key = key
this.size = size
}
}

View file

@ -1,7 +1,7 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as cache from '@actions/cache'
import * as deprecator from './deprecation-collector'
import {getCache} from './caching/cache-api'
import {SUMMARY_ENV_VAR} from '@actions/core/lib/summary'
import path from 'path'
@ -104,8 +104,12 @@ export enum DependencyGraphOption {
}
export class CacheConfig {
isFeatureAvailable(): boolean {
return getCache().isAvailable()
}
isCacheDisabled(): boolean {
if (!cache.isFeatureAvailable()) {
if (!this.isFeatureAvailable()) {
return true
}

View file

@ -3,13 +3,13 @@ import * as os from 'os'
import * as path from 'path'
import * as httpm from 'typed-rest-client/HttpClient'
import * as core from '@actions/core'
import * as cache from '@actions/cache'
import * as toolCache from '@actions/tool-cache'
import {determineGradleVersion, findGradleExecutableOnPath, versionIsAtLeast} from './gradle'
import * as gradlew from './gradlew'
import {handleCacheFailure} from '../caching/cache-utils'
import {handleCacheFailure, saveCache, restoreCache} from '../caching/cache-utils'
import {CacheConfig} from '../configuration'
import {CacheEntryListener} from '../caching/cache-reporting'
const gradleVersionsBaseUrl = 'https://services.gradle.org/versions'
@ -167,8 +167,9 @@ async function downloadAndCacheGradleDistribution(versionInfo: GradleVersionInfo
}
const cacheKey = `gradle-${versionInfo.version}`
const listener = new CacheEntryListener(cacheKey)
try {
const restoreKey = await cache.restoreCache([downloadPath], cacheKey)
const restoreKey = await restoreCache([downloadPath], cacheKey, [], listener)
if (restoreKey) {
core.info(`Restored Gradle distribution ${cacheKey} from cache to ${downloadPath}`)
return downloadPath
@ -182,7 +183,7 @@ async function downloadAndCacheGradleDistribution(versionInfo: GradleVersionInfo
if (!cacheConfig.isCacheReadOnly()) {
try {
await cache.saveCache([downloadPath], cacheKey)
await saveCache([downloadPath], cacheKey, listener)
} catch (error) {
handleCacheFailure(error, `Save Gradle distribution ${versionInfo.version} failed`)
}