Add cache-provider: external to skip Gradle User Home caching (#1059)

Users relying on an external mechanism to save/restore Gradle User Home
(e.g. Develocity Artifact Cache) previously had to set cache-disabled:
true, which is confusing since caching isn't actually disabled — it's
just not managed by this action — and the Job Summary misleadingly
reported caching as "Disabled".

cache-provider: external skips Gradle User Home restore/save (same as
cache-disabled) but reports a distinct "External" status in the Job
Summary, explaining that caching is handled by another provider.

---------

Co-authored-by: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Daz DeBoer
2026-08-25 09:44:41 -06:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 4ca152ae04
commit 910b061d4d
11 changed files with 162 additions and 12 deletions
+14 -5
View File
@@ -5,14 +5,18 @@ import {pathToFileURL} from 'url'
import {CacheConfig, CacheProvider} from './configuration'
import {BasicCacheService} from './cache-service-basic'
import {BuildResult} from './build-results'
import {CacheOptions, CacheReport, CacheService} from './cache-service'
import {CacheOptions, CacheReport, CacheService, CacheStatus} from './cache-service'
import {ProviderNote} from './caching-report'
const ENHANCED_CACHE_MESSAGE = `Enhanced Caching: This build is using the proprietary 'gradle-actions-caching' provider for optimized caching support. See https://github.com/gradle/actions/blob/main/DISTRIBUTION.md for terms of use and opt-out instructions.`
const BASIC_CACHE_MESSAGE = `Basic Caching: This build uses the basic open-source caching provider. For faster builds and advanced features, consider switching to the Enhanced Caching provider. See https://github.com/gradle/actions/blob/main/DISTRIBUTION.md for details.`
const EXTERNAL_CACHE_MESSAGE = `External Caching: Gradle User Home is managed by an external caching provider. This action will not restore or save Gradle User Home.`
class NoOpCacheService implements CacheService {
constructor(private readonly status: CacheStatus) {}
async restore(_gradleUserHome: string, _cacheOptions: CacheOptions): Promise<void> {
return
}
@@ -22,14 +26,19 @@ class NoOpCacheService implements CacheService {
_buildResults: BuildResult[],
_cacheOptions: CacheOptions
): Promise<CacheReport> {
return {status: 'disabled', entries: []}
return {status: this.status, entries: []}
}
}
export async function getCacheService(cacheConfig: CacheConfig): Promise<CacheService> {
if (cacheConfig.getCacheProvider() === CacheProvider.External) {
logCacheMessage(EXTERNAL_CACHE_MESSAGE)
return new NoOpCacheService('external')
}
if (cacheConfig.isCacheDisabled()) {
logCacheMessage('Cache is disabled: will not restore state from previous builds.')
return new NoOpCacheService()
return new NoOpCacheService('disabled')
}
if (cacheConfig.getCacheProvider() === CacheProvider.Basic) {
@@ -43,10 +52,10 @@ export async function getCacheService(cacheConfig: CacheConfig): Promise<CacheSe
/**
* Identifies the caching provider for the Job Summary. Returns `undefined` when
* caching is disabled, since no provider is engaged in that case.
* caching is disabled or managed externally, since no provider is engaged in that case.
*/
export function getProviderNote(cacheConfig: CacheConfig): ProviderNote | undefined {
if (cacheConfig.isCacheDisabled()) {
if (cacheConfig.isCacheDisabled() || cacheConfig.getCacheProvider() === CacheProvider.External) {
return undefined
}
return cacheConfig.getCacheProvider() === CacheProvider.Basic ? {kind: 'basic'} : {kind: 'enhanced'}
+12 -2
View File
@@ -15,10 +15,20 @@ export interface CacheOptions {
}
export type CacheStatus =
'enabled' | 'read-only' | 'write-only' | 'disabled' | 'disabled-existing-home' | 'not-available'
| 'enabled' // Gradle User Home was restored from and saved to the cache
| 'read-only' // restored from the cache, but not saved
| 'write-only' // saved to the cache, but not restored
| 'disabled' // caching was turned off via the cache-disabled parameter
| 'disabled-existing-home' // a pre-existing Gradle User Home was found, so caching was skipped
| 'not-available' // the GitHub Actions cache service could not be reached
| 'external' // Gradle User Home is cached by an external provider, not by this action
export type CacheCleanupStatus =
'enabled' | 'disabled-param' | 'disabled-failure' | 'disabled-config-cache-hit' | 'disabled-readonly'
| 'enabled' // stale files were purged from Gradle User Home before saving
| 'disabled-param' // disabled via action parameter
| 'disabled-failure' // skipped due to a build failure
| 'disabled-config-cache-hit' // skipped due to configuration-cache reuse
| 'disabled-readonly' // always disabled when the cache is read-only
export type ProjectCacheStatus =
| 'not-enabled' // the hidden opt-in env var was not set (rendered as nothing)
+9 -2
View File
@@ -17,7 +17,8 @@ const STATUS_COPY: Record<CacheStatus, string> = {
'write-only': `[Cache was write-only](${DOCS}#using-the-cache-write-only) — Gradle User Home was not restored from the cache.`,
disabled: `[Caching was disabled](${DOCS}#disabling-caching) — Gradle User Home was not restored from or saved to the cache.`,
'disabled-existing-home': `⚠️ [Caching was skipped](${DOCS}#overwriting-an-existing-gradle-user-home) — a pre-existing Gradle User Home was found, so the cache was not restored or saved.`,
'not-available': `Caching is not available — the GitHub Actions cache service could not be reached, so Gradle User Home was not restored or saved.`
'not-available': `Caching is not available — the GitHub Actions cache service could not be reached, so Gradle User Home was not restored or saved.`,
external: `[Gradle User Home is cached externally](${DOCS}#using-an-external-cache-provider) — this action did not restore or save the Gradle User Home.`
}
const CLEANUP_COPY: Record<CacheCleanupStatus, string> = {
@@ -63,7 +64,13 @@ function isActive(status: CacheStatus): boolean {
function renderHeading(status: CacheStatus, providerNote?: ProviderNote): string {
if (!isActive(status)) {
const label =
status === 'disabled-existing-home' ? 'Skipped' : status === 'not-available' ? 'Unavailable' : 'Disabled'
status === 'disabled-existing-home'
? 'Skipped'
: status === 'not-available'
? 'Unavailable'
: status === 'external'
? 'External'
: 'Disabled'
return `<h4>Gradle State Caching - ${label}</h4>`
}
+7 -2
View File
@@ -175,14 +175,19 @@ export class CacheConfig {
case 'enhanced':
case '':
return CacheProvider.Enhanced
case 'external':
return CacheProvider.External
}
throw TypeError(`The value '${val}' is not valid for 'cache-provider'. Valid values are: [basic, enhanced].`)
throw TypeError(
`The value '${val}' is not valid for 'cache-provider'. Valid values are: [basic, enhanced, external].`
)
}
}
export enum CacheProvider {
Basic = 'basic',
Enhanced = 'enhanced'
Enhanced = 'enhanced',
External = 'external'
}
export enum CacheCleanupOption {
@@ -45,6 +45,51 @@ describe('getCacheService selection logic', () => {
expect(service).toBeInstanceOf(BasicCacheService)
})
it('returns NoOpCacheService reporting "external" when cache-provider is external', async () => {
const {getCacheService} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => false,
getCacheProvider: () => CacheProvider.External
} as unknown as CacheConfig
const service = await getCacheService(mockConfig)
const report = await service.save('/home/.gradle', [], {
disabled: false,
readOnly: false,
writeOnly: false,
overwriteExisting: false,
strictMatch: false,
cleanup: 'never',
includes: [],
excludes: []
})
expect(report.status).toBe('external')
expect(report.entries).toHaveLength(0)
})
it('reports "external" even when cache-disabled is also true', async () => {
const {getCacheService} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => true,
getCacheProvider: () => CacheProvider.External
} as unknown as CacheConfig
const service = await getCacheService(mockConfig)
const report = await service.save('/home/.gradle', [], {
disabled: false,
readOnly: false,
writeOnly: false,
overwriteExisting: false,
strictMatch: false,
cleanup: 'never',
includes: [],
excludes: []
})
expect(report.status).toBe('external')
})
describe('getProviderNote', () => {
it('returns undefined when cache is disabled', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader')
@@ -56,6 +101,16 @@ describe('getCacheService selection logic', () => {
expect(getProviderNote(mockConfig)).toBeUndefined()
})
it('returns undefined when cache-provider is external', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => false,
getCacheProvider: () => CacheProvider.External
} as unknown as CacheConfig
expect(getProviderNote(mockConfig)).toBeUndefined()
})
it('returns basic note for the basic provider', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader')
const mockConfig = {
+10
View File
@@ -142,4 +142,14 @@ describe('renderCachingReport', () => {
expect(md).toContain('<h4>Gradle State Caching - Unavailable</h4>')
expect(md).not.toContain('<details>')
})
it('renders a compact external report with no provider note', () => {
const report: CacheReport = {status: 'external', entries: []}
const md = renderCachingReport(report, undefined)
expect(md).toContain('<h4>Gradle State Caching - External</h4>')
expect(md).toContain('cached externally')
expect(md).not.toContain('<details>')
expect(md).not.toContain('DISTRIBUTION.md')
})
})