From 44db8041a5b0a982b8abc8ccc3c3a1476874eb54 Mon Sep 17 00:00:00 2001 From: daz Date: Thu, 30 Jan 2025 13:47:12 -0700 Subject: [PATCH 1/2] Use typed-rest-client/HttpClient in preference to @actions/HttpClient --- sources/package.json | 1 - sources/src/develocity/short-lived-token.ts | 2 +- sources/src/execution/provision.ts | 2 +- sources/src/wrapper-validation/checksums.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sources/package.json b/sources/package.json index 4e430cd..a173808 100644 --- a/sources/package.json +++ b/sources/package.json @@ -38,7 +38,6 @@ "@actions/exec": "1.1.1", "@actions/github": "6.0.0", "@actions/glob": "0.5.0", - "@actions/http-client": "2.2.3", "@actions/tool-cache": "2.0.2", "@octokit/rest": "21.1.0", "@octokit/webhooks-types": "7.6.1", diff --git a/sources/src/develocity/short-lived-token.ts b/sources/src/develocity/short-lived-token.ts index c059f96..d0cbbce 100644 --- a/sources/src/develocity/short-lived-token.ts +++ b/sources/src/develocity/short-lived-token.ts @@ -67,7 +67,7 @@ export async function getToken(accessKey: string, expiry: string): Promise>() From e03531acf198cb35dc4286074bba9aeaf8857710 Mon Sep 17 00:00:00 2001 From: daz Date: Thu, 30 Jan 2025 14:23:30 -0700 Subject: [PATCH 2/2] 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 --- sources/src/caching/cache-api.ts | 37 ++++++++++++++++ sources/src/caching/cache-reporting.ts | 10 ++++- sources/src/caching/cache-utils.ts | 48 +++++++------------- sources/src/caching/github-actions-cache.ts | 49 +++++++++++++++++++++ sources/src/configuration.ts | 8 +++- sources/src/execution/provision.ts | 9 ++-- 6 files changed, 120 insertions(+), 41 deletions(-) create mode 100644 sources/src/caching/cache-api.ts create mode 100644 sources/src/caching/github-actions-cache.ts diff --git a/sources/src/caching/cache-api.ts b/sources/src/caching/cache-api.ts new file mode 100644 index 0000000..3305a02 --- /dev/null +++ b/sources/src/caching/cache-api.ts @@ -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 + + /** + * 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 +} + +export declare class CacheResult { + key: string + size?: number + constructor(key: string, size?: number) +} diff --git a/sources/src/caching/cache-reporting.ts b/sources/src/caching/cache-reporting.ts index cd85fe7..ad4d334 100644 --- a/sources/src/caching/cache-reporting.ts +++ b/sources/src/caching/cache-reporting.ts @@ -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 diff --git a/sources/src/caching/cache-utils.ts b/sources/src/caching/cache-utils.ts index 3d5e3b3..cd88339 100644 --- a/sources/src/caching/cache-utils.ts +++ b/sources/src/caching/cache-utils.ts @@ -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 { +): Promise { 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 { +export async function saveCache(cachePaths: string[], cacheKey: string, listener: CacheEntryListener): Promise { 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) } } diff --git a/sources/src/caching/github-actions-cache.ts b/sources/src/caching/github-actions-cache.ts new file mode 100644 index 0000000..7973ae6 --- /dev/null +++ b/sources/src/caching/github-actions-cache.ts @@ -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 { + // 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 { + 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 + } +} diff --git a/sources/src/configuration.ts b/sources/src/configuration.ts index a99689d..e0afc23 100644 --- a/sources/src/configuration.ts +++ b/sources/src/configuration.ts @@ -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 } diff --git a/sources/src/execution/provision.ts b/sources/src/execution/provision.ts index f3a9a56..44fceea 100644 --- a/sources/src/execution/provision.ts +++ b/sources/src/execution/provision.ts @@ -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`) }