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
+17
View File
@@ -132,6 +132,23 @@ jobs:
working-directory: .github/workflow-samples/kotlin-dsl working-directory: .github/workflow-samples/kotlin-dsl
run: ./gradlew assemble run: ./gradlew assemble
cache-provider-external:
needs: build-distribution
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-provider: external
- name: Build kotlin-dsl project
working-directory: .github/workflow-samples/kotlin-dsl
run: ./gradlew assemble
basic-caching: basic-caching:
needs: build-distribution needs: build-distribution
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -134,6 +134,30 @@ jobs:
script: | script: |
core.setFailed('No Build Scan detected') core.setFailed('No Build Scan detected')
# Test that build scans are captured when caching is external
caching-config-cache-provider-external:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-provider: external
- name: Build using Gradle wrapper
id: gradle
working-directory: .github/workflow-samples/groovy-dsl
run: ./gradlew help
- name: Check Build Scan url is captured
if: ${{ !steps.gradle.outputs.build-scan-url }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
core.setFailed('No Build Scan detected')
# Test seed the cache with cache-write-only and verify with cache-read-only # Test seed the cache with cache-write-only and verify with cache-read-only
caching-config-seed-write-only: caching-config-seed-write-only:
env: env:
+1
View File
@@ -31,6 +31,7 @@ inputs:
Specifies the caching implementation to use. Specifies the caching implementation to use.
'enhanced' (default) uses the full-featured commercial caching service (gradle-actions-caching). 'enhanced' (default) uses the full-featured commercial caching service (gradle-actions-caching).
'basic' uses a simple open-source caching implementation based on GitHub Actions cache. 'basic' uses a simple open-source caching implementation based on GitHub Actions cache.
'external' disables Gradle User Home caching by this action, for use when Gradle User Home is already saved and restored by another mechanism (e.g. Develocity Artifact Cache).
required: false required: false
default: 'enhanced' default: 'enhanced'
+12 -1
View File
@@ -133,6 +133,7 @@ You choose which provider to use via the `cache-provider` input:
- **`enhanced`** (default): Uses the full-featured commercial `gradle-actions-caching` library. Provides advanced features like fine-grained cache entries, intelligent cache cleanup, and deduplication. See [Enhanced Caching](#enhanced-caching) for details. - **`enhanced`** (default): Uses the full-featured commercial `gradle-actions-caching` library. Provides advanced features like fine-grained cache entries, intelligent cache cleanup, and deduplication. See [Enhanced Caching](#enhanced-caching) for details.
- **`basic`**: A fully open-source (MIT) caching implementation built on the standard GitHub Actions cache (`@actions/cache`). Uses the same caching strategy as `actions/setup-java` with `cache: gradle`. See [Basic Caching](#basic-caching) for details. - **`basic`**: A fully open-source (MIT) caching implementation built on the standard GitHub Actions cache (`@actions/cache`). Uses the same caching strategy as `actions/setup-java` with `cache: gradle`. See [Basic Caching](#basic-caching) for details.
- **`external`**: Disables Gradle User Home caching by this action, for use when another mechanism already saves and restores Gradle User Home. See [Using an external cache provider](#using-an-external-cache-provider) for details.
```yaml ```yaml
# Use the open-source basic cache provider # Use the open-source basic cache provider
@@ -185,7 +186,17 @@ Specifically:
- Avoid using `actions/cache` configured to cache the Gradle User Home, [as described in this example](https://github.com/actions/cache/blob/main/examples.md#java---gradle). - Avoid using `actions/cache` configured to cache the Gradle User Home, [as described in this example](https://github.com/actions/cache/blob/main/examples.md#java---gradle).
- Avoid using `actions/setup-java` with the `cache: gradle` option, [as described here](https://github.com/actions/setup-java#caching-gradle-dependencies). - Avoid using `actions/setup-java` with the `cache: gradle` option, [as described here](https://github.com/actions/setup-java#caching-gradle-dependencies).
Using either of these mechanisms may interfere with the caching provided by this action. If you choose to use a different mechanism to save and restore the Gradle User Home, you should disable the caching provided by this action, as described above. Using either of these mechanisms may interfere with the caching provided by this action. If you choose to use a different mechanism to save and restore the Gradle User Home, you should disable the caching provided by this action, as described above, or [use `cache-provider: external`](#using-an-external-cache-provider) if that mechanism is a dedicated Gradle User Home cache.
#### Using an external cache provider
If Gradle User Home is already saved and restored by another mechanism — for example the [Develocity Artifact Cache](https://github.com/gradle/actions-caching) — set `cache-provider: external` rather than `cache-disabled: true`.
Both settings stop `setup-gradle` from restoring or saving Gradle User Home, but `cache-provider: external` reflects the actual reason: caching is still happening, just not through this action. It also updates the Job Summary to say that caching was handled externally, rather than implying that caching was turned off.
```yaml
cache-provider: external
```
## Enhanced Caching ## Enhanced Caching
+1
View File
@@ -14,6 +14,7 @@ inputs:
Specifies the caching implementation to use. Specifies the caching implementation to use.
'enhanced' (default) uses the full-featured commercial caching service (gradle-actions-caching). 'enhanced' (default) uses the full-featured commercial caching service (gradle-actions-caching).
'basic' uses a simple open-source caching implementation based on GitHub Actions cache. 'basic' uses a simple open-source caching implementation based on GitHub Actions cache.
'external' disables Gradle User Home caching by this action, for use when Gradle User Home is already saved and restored by another mechanism (e.g. Develocity Artifact Cache).
required: false required: false
default: 'enhanced' default: 'enhanced'
+14 -5
View File
@@ -5,14 +5,18 @@ import {pathToFileURL} from 'url'
import {CacheConfig, CacheProvider} from './configuration' import {CacheConfig, CacheProvider} from './configuration'
import {BasicCacheService} from './cache-service-basic' import {BasicCacheService} from './cache-service-basic'
import {BuildResult} from './build-results' 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' 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 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 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 { class NoOpCacheService implements CacheService {
constructor(private readonly status: CacheStatus) {}
async restore(_gradleUserHome: string, _cacheOptions: CacheOptions): Promise<void> { async restore(_gradleUserHome: string, _cacheOptions: CacheOptions): Promise<void> {
return return
} }
@@ -22,14 +26,19 @@ class NoOpCacheService implements CacheService {
_buildResults: BuildResult[], _buildResults: BuildResult[],
_cacheOptions: CacheOptions _cacheOptions: CacheOptions
): Promise<CacheReport> { ): Promise<CacheReport> {
return {status: 'disabled', entries: []} return {status: this.status, entries: []}
} }
} }
export async function getCacheService(cacheConfig: CacheConfig): Promise<CacheService> { export async function getCacheService(cacheConfig: CacheConfig): Promise<CacheService> {
if (cacheConfig.getCacheProvider() === CacheProvider.External) {
logCacheMessage(EXTERNAL_CACHE_MESSAGE)
return new NoOpCacheService('external')
}
if (cacheConfig.isCacheDisabled()) { if (cacheConfig.isCacheDisabled()) {
logCacheMessage('Cache is disabled: will not restore state from previous builds.') logCacheMessage('Cache is disabled: will not restore state from previous builds.')
return new NoOpCacheService() return new NoOpCacheService('disabled')
} }
if (cacheConfig.getCacheProvider() === CacheProvider.Basic) { 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 * 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 { export function getProviderNote(cacheConfig: CacheConfig): ProviderNote | undefined {
if (cacheConfig.isCacheDisabled()) { if (cacheConfig.isCacheDisabled() || cacheConfig.getCacheProvider() === CacheProvider.External) {
return undefined return undefined
} }
return cacheConfig.getCacheProvider() === CacheProvider.Basic ? {kind: 'basic'} : {kind: 'enhanced'} return cacheConfig.getCacheProvider() === CacheProvider.Basic ? {kind: 'basic'} : {kind: 'enhanced'}
+12 -2
View File
@@ -15,10 +15,20 @@ export interface CacheOptions {
} }
export type CacheStatus = 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 = 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 = export type ProjectCacheStatus =
| 'not-enabled' // the hidden opt-in env var was not set (rendered as nothing) | '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.`, '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: `[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.`, '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> = { const CLEANUP_COPY: Record<CacheCleanupStatus, string> = {
@@ -63,7 +64,13 @@ function isActive(status: CacheStatus): boolean {
function renderHeading(status: CacheStatus, providerNote?: ProviderNote): string { function renderHeading(status: CacheStatus, providerNote?: ProviderNote): string {
if (!isActive(status)) { if (!isActive(status)) {
const label = 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>` return `<h4>Gradle State Caching - ${label}</h4>`
} }
+7 -2
View File
@@ -175,14 +175,19 @@ export class CacheConfig {
case 'enhanced': case 'enhanced':
case '': case '':
return CacheProvider.Enhanced 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 { export enum CacheProvider {
Basic = 'basic', Basic = 'basic',
Enhanced = 'enhanced' Enhanced = 'enhanced',
External = 'external'
} }
export enum CacheCleanupOption { export enum CacheCleanupOption {
@@ -45,6 +45,51 @@ describe('getCacheService selection logic', () => {
expect(service).toBeInstanceOf(BasicCacheService) 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', () => { describe('getProviderNote', () => {
it('returns undefined when cache is disabled', async () => { it('returns undefined when cache is disabled', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader') const {getProviderNote} = await import('../../src/cache-service-loader')
@@ -56,6 +101,16 @@ describe('getCacheService selection logic', () => {
expect(getProviderNote(mockConfig)).toBeUndefined() 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 () => { it('returns basic note for the basic provider', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader') const {getProviderNote} = await import('../../src/cache-service-loader')
const mockConfig = { const mockConfig = {
+10
View File
@@ -142,4 +142,14 @@ describe('renderCachingReport', () => {
expect(md).toContain('<h4>Gradle State Caching - Unavailable</h4>') expect(md).toContain('<h4>Gradle State Caching - Unavailable</h4>')
expect(md).not.toContain('<details>') 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')
})
}) })