Compare commits

...

8 Commits

Author SHA1 Message Date
Daz DeBoer 5082e2053e Drop DISTRIBUTION.md reference from the Basic provider note
The terms-of-use / opt-out link is only relevant to the proprietary
Enhanced provider, so the Basic note now ends after the upgrade
suggestion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:07:23 -06:00
Daz DeBoer e5857b8ffa Tweak caching report title and provider note wording
- Title now reads "Gradle State Caching — <icon> <Provider> (<status>)",
  consistent across all variants, with the status qualifier restored.
- Make both provider notes parallel: "Enhanced Caching uses the
  proprietary ..." / "Basic Caching uses the basic open-source ...".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:05:19 -06:00
Daz DeBoer f5cb57ae79 Refine caching report layout and copy
- Render the heading as <h4> so it displays as a heading (markdown
  ATX headings were rendered literally in the Job Summary).
- Heading reads "Gradle Caching — <Enhanced|Basic> Provider"; drop the
  status suffix since status now lives inside the details expando.
- Provider note: lead with a linked "Enhanced Caching" / "Basic Caching"
  to the docs; the proprietary provider name is now inline code.
- Move the status and cache-cleanup messages inside the details expando.
  The clickable summary now reads "Entries: N restored, M saved
  (expand for more details)".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 22:47:32 -06:00
Daz DeBoer cbc6f4a545 Redesign the caching Job Summary
Render the caching report from structured CacheReport data using a
single unified layout, replacing the three divergent code paths
(NoOp / basic / enhanced) that previously each rendered their own
markdown.

Every variant now shares one skeleton: a section heading
(`#### <icon> Gradle Caching — <Provider> (<status>)`), a status line,
an integrated provider note, and an expandable cache-entry-details
section. The Enhanced/Basic provider note is woven into the report
under the heading and is now shown unconditionally (no longer gated on
license acceptance). The two disabled variants (explicitly disabled,
and skipped due to a pre-existing Gradle User Home) render as compact
callouts with no expandable section.

- caching-report.ts: new central renderer + all framing copy.
- cache-service.ts: CacheReport/CacheEntryReport/status types; save()
  returns CacheReport.
- cache-service-loader.ts: NoOp returns a CacheReport, drop
  LicenseWarningCacheService, add getProviderNote().
- cache-service-basic.ts: build a CacheReport, so basic caching now
  also gets expandable entry details.
- job-summary.ts / setup-gradle.ts: thread CacheReport + ProviderNote.
- configuration.ts: remove now-unused isCacheLicenseAccepted().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 21:49:25 -06:00
Daz DeBoer 483fcf682f TEMP: vendor UNRELEASED gradle-actions-caching v0.7.0
Local hand-copied build of gradle-actions-caching v0.7.0, which returns
a structured CacheReport from save(). This commit exists only so the
source changes can be built and tested against the new contract.

Remove this commit once v0.7.0 is released and vendored via the official
CI-release workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 21:49:15 -06:00
Daz DeBoer 40874b82b7 Fix whitespace for basic cache message 2026-06-10 15:45:26 -07:00
Daz DeBoer a03a9c198b Improve basic cache messages 2026-06-10 15:36:52 -07:00
Daz DeBoer 2577c609e7 Reduce the visual impact of caching info messages 2026-06-10 15:25:34 -07:00
13 changed files with 498 additions and 92 deletions
+75 -13
View File
@@ -4,7 +4,9 @@ import * as glob from '@actions/glob'
import * as path from 'path'
import {BuildResult} from './build-results'
import {CacheOptions, CacheService} from './cache-service'
import {CacheEntryReport, CacheOptions, CacheReport, CacheService} from './cache-service'
const ENTRY_NAME = 'Gradle User Home'
const PRIMARY_KEY_STATE = 'BASIC_CACHE_PRIMARY_KEY'
const RESTORED_KEY_STATE = 'BASIC_CACHE_RESTORED_KEY'
@@ -40,21 +42,39 @@ export class BasicCacheService implements CacheService {
}
}
async save(gradleUserHome: string, _buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string> {
if (cacheOptions.readOnly) {
const restoredKey = core.getState(RESTORED_KEY_STATE)
if (restoredKey) {
return `Basic caching was read-only. Restored from cache key \`${restoredKey}\`.`
}
return 'Basic caching was read-only. No cache entry was found to restore.'
}
async save(gradleUserHome: string, _buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<CacheReport> {
const primaryKey = core.getState(PRIMARY_KEY_STATE)
const restoredKey = core.getState(RESTORED_KEY_STATE)
if (cacheOptions.readOnly) {
return {
status: 'read-only',
entries: [
entryReport({
primaryKey,
restoredKey,
restoredOutcome: restoredKey
? '(Entry restored: exact match found)'
: '(Entry not restored: no match found)',
savedOutcome: '(Entry not saved: cache is read-only)'
})
]
}
}
if (restoredKey === primaryKey) {
core.info(`Basic caching restored entry with key \`${primaryKey}\`. Save was skipped.`)
return `Basic caching restored entry with key \`${primaryKey}\`. Save was skipped.`
return {
status: 'enabled',
entries: [
entryReport({
primaryKey,
restoredKey,
restoredOutcome: '(Entry restored: exact match found)',
savedOutcome: '(Entry not saved: entry with key already exists)'
})
]
}
}
const cachePaths = getCachePaths(gradleUserHome)
@@ -62,14 +82,56 @@ export class BasicCacheService implements CacheService {
try {
await cache.saveCache(cachePaths, primaryKey)
core.info(`Basic caching saved entry with key: ${primaryKey}`)
return `Basic caching saved entry with key \`${primaryKey}\`.`
return {
status: 'enabled',
entries: [
entryReport({
primaryKey,
restoredKey,
savedKey: primaryKey,
restoredOutcome: restoredKey
? '(Entry restored: exact match found)'
: '(Entry not restored: no match found)',
savedOutcome: '(Entry saved)'
})
]
}
} catch (error) {
core.warning(`Basic caching failed to save entry with key \`${primaryKey}\`: ${error}`)
return `Basic caching save failed: ${error}`
return {
status: 'enabled',
entries: [
entryReport({
primaryKey,
restoredKey,
restoredOutcome: restoredKey
? '(Entry restored: exact match found)'
: '(Entry not restored: no match found)',
savedOutcome: `(Entry not saved: ${error})`
})
]
}
}
}
}
function entryReport(opts: {
primaryKey: string
restoredKey?: string
savedKey?: string
restoredOutcome: string
savedOutcome: string
}): CacheEntryReport {
return {
entryName: ENTRY_NAME,
requestedKey: opts.primaryKey || undefined,
restoredKey: opts.restoredKey || undefined,
restoredOutcome: opts.restoredOutcome,
savedKey: opts.savedKey || undefined,
savedOutcome: opts.savedOutcome
}
}
function getCachePaths(gradleUserHome: string): string[] {
return [path.join(gradleUserHome, 'caches'), path.join(gradleUserHome, 'wrapper')]
}
+21 -48
View File
@@ -5,57 +5,24 @@ import {pathToFileURL} from 'url'
import {CacheConfig, CacheProvider} from './configuration'
import {BasicCacheService} from './cache-service-basic'
import {BuildResult} from './build-results'
import {CacheOptions, CacheService} from './cache-service'
const NOOP_CACHING_REPORT = `
[Cache was disabled](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#disabling-caching). Gradle User Home was not restored from or saved to the cache.
`
import {CacheOptions, CacheReport, CacheService} 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 ENHANCED_CACHE_SUMMARY = `
> [!NOTE]
> ### ⚡️ Enhanced Caching enabled
> This build provides optimized caching support via the proprietary **gradle-actions-caching** provider.
> See [DISTRIBUTION.md](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 open-source caching provider for reliable, path-based caching of Gradle dependencies. Upgrade available: 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_SUMMARY = `
> [!NOTE]
> ### 🛡️ Basic Caching enabled
> This build uses the open-source caching provider for reliable, path-based caching of Gradle dependencies.
>
> **Upgrade Available:** For faster builds and advanced features, consider switching to the **Enhanced Caching** provider.
> See [DISTRIBUTION.md](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.`
class NoOpCacheService implements CacheService {
async restore(_gradleUserHome: string, _cacheOptions: CacheOptions): Promise<void> {
return
}
async save(_gradleUserHome: string, _buildResults: BuildResult[], _cacheOptions: CacheOptions): Promise<string> {
return NOOP_CACHING_REPORT
}
}
class LicenseWarningCacheService implements CacheService {
private delegate: CacheService
private summary: string
constructor(delegate: CacheService, summary: string) {
this.delegate = delegate
this.summary = summary
}
async restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void> {
await this.delegate.restore(gradleUserHome, cacheOptions)
}
async save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string> {
const cachingReport = await this.delegate.save(gradleUserHome, buildResults, cacheOptions)
return `${cachingReport}\n${this.summary}`
async save(
_gradleUserHome: string,
_buildResults: BuildResult[],
_cacheOptions: CacheOptions
): Promise<CacheReport> {
return {status: 'disabled', entries: []}
}
}
@@ -67,16 +34,22 @@ export async function getCacheService(cacheConfig: CacheConfig): Promise<CacheSe
if (cacheConfig.getCacheProvider() === CacheProvider.Basic) {
logCacheMessage(BASIC_CACHE_MESSAGE)
return new LicenseWarningCacheService(new BasicCacheService(), BASIC_CACHE_SUMMARY)
return new BasicCacheService()
}
logCacheMessage(ENHANCED_CACHE_MESSAGE)
const cacheService = await loadVendoredCacheService()
if (cacheConfig.isCacheLicenseAccepted()) {
return cacheService
}
return loadVendoredCacheService()
}
return new LicenseWarningCacheService(cacheService, ENHANCED_CACHE_SUMMARY)
/**
* Identifies the caching provider for the Job Summary. Returns `undefined` when
* caching is disabled, since no provider is engaged in that case.
*/
export function getProviderNote(cacheConfig: CacheConfig): ProviderNote | undefined {
if (cacheConfig.isCacheDisabled()) {
return undefined
}
return cacheConfig.getCacheProvider() === CacheProvider.Basic ? {kind: 'basic'} : {kind: 'enhanced'}
}
export async function loadVendoredCacheService(): Promise<CacheService> {
+39 -1
View File
@@ -12,7 +12,45 @@ export interface CacheOptions {
excludes: string[]
}
export type CacheStatus =
| 'enabled'
| 'read-only'
| 'write-only'
| 'disabled'
| 'disabled-existing-home'
| 'not-available'
export type CacheCleanupStatus =
| 'enabled'
| 'disabled-param'
| 'disabled-failure'
| 'disabled-config-cache-hit'
| 'disabled-readonly'
export interface CacheEntryReport {
entryName: string
requestedKey?: string
restoredKey?: string
restoredSize?: number
restoredTime?: number
restoredOutcome: string
savedKey?: string
savedSize?: number
savedTime?: number
savedOutcome: string
}
/**
* Structured result of a cache save operation. Rendering this into a human-readable
* Job Summary is handled centrally by `caching-report.ts`.
*/
export interface CacheReport {
status: CacheStatus
cleanup?: CacheCleanupStatus
entries: CacheEntryReport[]
}
export interface CacheService {
restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>
save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string>
save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<CacheReport>
}
+164
View File
@@ -0,0 +1,164 @@
import {CacheCleanupStatus, CacheEntryReport, CacheReport, CacheStatus} from './cache-service'
const DOCS = 'https://github.com/gradle/actions/blob/main/docs/setup-gradle.md'
const DISTRIBUTION = 'https://github.com/gradle/actions/blob/main/DISTRIBUTION.md'
/**
* Identifies the caching provider in use, so the report can attribute the cache
* and surface the relevant terms-of-use / upgrade information.
*/
export interface ProviderNote {
kind: 'enhanced' | 'basic'
}
const STATUS_COPY: Record<CacheStatus, string> = {
enabled: `[Cache was enabled](${DOCS}#caching-build-state-between-jobs) — Gradle User Home was restored from the cache and saved for use by subsequent jobs.`,
'read-only': `[Cache was read-only](${DOCS}#using-the-cache-read-only) — by default, the action only writes to the cache for jobs running on the default branch.`,
'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.`
}
const CLEANUP_COPY: Record<CacheCleanupStatus, string> = {
enabled: `[Cache cleanup](${DOCS}#configuring-cache-cleanup) purged stale files from Gradle User Home before saving.`,
'disabled-param': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) was disabled via action parameter.`,
'disabled-failure': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) was skipped due to a build failure. Use \`cache-cleanup: always\` to override.`,
'disabled-config-cache-hit': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) was skipped due to configuration-cache reuse.`,
'disabled-readonly': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) is always disabled when the cache is read-only.`
}
/**
* Renders a cache report into the unified Job Summary markdown, with a consistent
* skeleton across every variant: a section heading, a status line, an integrated
* provider note, and (when there are entries) an expandable details section.
*/
export function renderCachingReport(report: CacheReport, providerNote?: ProviderNote): string {
if (!isActive(report.status)) {
// Disabled / skipped / unavailable: a compact heading + status line, no expandable section.
return `${renderHeading(report.status, providerNote)}\n\n${STATUS_COPY[report.status]}\n`
}
const sections = [
renderHeading(report.status, providerNote),
renderProviderNote(providerNote),
// Status and cleanup messages live inside the details expando; if there are no entries
// to expand, fall back to showing the status line directly.
report.entries.length > 0 ? renderDetails(report) : STATUS_COPY[report.status]
]
return `${sections.filter(section => section !== undefined && section !== '').join('\n\n')}\n`
}
function isActive(status: CacheStatus): boolean {
return status === 'enabled' || status === 'read-only' || status === 'write-only'
}
function renderHeading(status: CacheStatus, providerNote?: ProviderNote): string {
if (!isActive(status)) {
const label =
status === 'disabled-existing-home' ? 'Skipped' : status === 'not-available' ? 'Unavailable' : 'Disabled'
return `<h4>Gradle State Caching — ${label}</h4>`
}
const icon = providerNote?.kind === 'basic' ? '🛡️' : '⚡'
const provider = providerNote?.kind === 'basic' ? 'Basic' : 'Enhanced'
const suffix = status === 'read-only' ? ' (read-only)' : status === 'write-only' ? ' (write-only)' : ''
return `<h4>Gradle State Caching — ${icon} ${provider}${suffix}</h4>`
}
function renderCleanupLine(cleanup?: CacheCleanupStatus): string | undefined {
return cleanup ? CLEANUP_COPY[cleanup] : undefined
}
function renderProviderNote(providerNote?: ProviderNote): string | undefined {
if (!providerNote) {
return undefined
}
if (providerNote.kind === 'enhanced') {
return `**[Enhanced Caching](${DOCS}#enhanced-caching)** uses the proprietary \`gradle-actions-caching\` provider. See [DISTRIBUTION.md](${DISTRIBUTION}) for terms of use and opt-out instructions.`
}
return `**[Basic Caching](${DOCS}#basic-caching)** uses the basic open-source provider. For faster builds and advanced features, consider the **[Enhanced Caching](${DOCS}#enhanced-caching)** provider.`
}
function renderDetails(report: CacheReport): string {
const restored = report.entries.filter(entry => entry.restoredKey).length
const saved = report.entries.filter(entry => entry.savedKey).length
const summary = `Entries: ${restored} restored, ${saved} saved (expand for more details)`
const cleanup = report.status === 'enabled' ? renderCleanupLine(report.cleanup) : undefined
const table = renderEntryTable(report.entries)
const pre = `<pre>\n${renderEntryDetails(report.entries)}</pre>`
const body = [STATUS_COPY[report.status], cleanup, table, pre].filter(Boolean).join('\n\n')
return `<details>
<summary>${summary}</summary>
${body}
</details>`
}
function renderEntryTable(entries: CacheEntryReport[]): string {
const hasMetrics = entries.some(
entry => entry.restoredSize || entry.restoredTime || entry.savedSize || entry.savedTime
)
if (!hasMetrics) {
return ''
}
return `<table>
<tr><td></td><th>Count</th><th>Total Size (Mb)</th><th>Total Time (ms)</th></tr>
<tr><td>Entries Restored</td>
<td>${getCount(entries, e => e.restoredSize)}</td>
<td>${getSize(entries, e => e.restoredSize)}</td>
<td>${getTime(entries, e => e.restoredTime)}</td>
</tr>
<tr><td>Entries Saved</td>
<td>${getCount(entries, e => e.savedSize)}</td>
<td>${getSize(entries, e => e.savedSize)}</td>
<td>${getTime(entries, e => e.savedTime)}</td>
</tr>
</table>`
}
function renderEntryDetails(entries: CacheEntryReport[]): string {
return entries
.map(
entry => `Entry: ${entry.entryName}
Requested Key : ${entry.requestedKey ?? ''}
Restored Key : ${entry.restoredKey ?? ''}
Size: ${formatSize(entry.restoredSize)}
Time: ${formatTime(entry.restoredTime)}
${entry.restoredOutcome}
Saved Key : ${entry.savedKey ?? ''}
Size: ${formatSize(entry.savedSize)}
Time: ${formatTime(entry.savedTime)}
${entry.savedOutcome}
`
)
.join('---\n')
}
function getCount(entries: CacheEntryReport[], predicate: (value: CacheEntryReport) => number | undefined): number {
return entries.filter(e => predicate(e)).length
}
function getSize(entries: CacheEntryReport[], predicate: (value: CacheEntryReport) => number | undefined): number {
const bytes = entries.map(e => predicate(e) ?? 0).reduce((p, v) => p + v, 0)
return Math.round(bytes / (1024 * 1024))
}
function getTime(entries: CacheEntryReport[], predicate: (value: CacheEntryReport) => number | undefined): number {
return entries.map(e => predicate(e) ?? 0).reduce((p, v) => p + v, 0)
}
function formatSize(bytes: number | undefined): string {
if (bytes === undefined || bytes === 0) {
return ''
}
return `${Math.round(bytes / (1024 * 1024))} MB (${bytes} B)`
}
function formatTime(ms: number | undefined): string {
if (ms === undefined || ms === 0) {
return ''
}
return `${ms} ms`
}
-5
View File
@@ -167,11 +167,6 @@ export class CacheConfig {
return core.getMultilineInput('gradle-home-cache-excludes')
}
isCacheLicenseAccepted(): boolean {
const dvConfig = new DevelocityConfig()
return dvConfig.getDevelocityAccessKey() !== '' || dvConfig.hasTermsOfUseAgreement()
}
getCacheProvider(): CacheProvider {
const val = core.getInput('cache-provider')
switch (val.toLowerCase().trim()) {
+5 -1
View File
@@ -2,12 +2,15 @@ import * as core from '@actions/core'
import * as github from '@actions/github'
import {BuildResult} from './build-results'
import {CacheReport} from './cache-service'
import {ProviderNote, renderCachingReport} from './caching-report'
import {DependencyGraphConfig, getActionId, getGithubToken, getJobMatrix, SummaryConfig} from './configuration'
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
export async function generateJobSummary(
buildResults: BuildResult[],
cachingReport: string,
cacheReport: CacheReport,
providerNote: ProviderNote | undefined,
config: SummaryConfig
): Promise<void> {
const errors = renderErrors()
@@ -18,6 +21,7 @@ export async function generateJobSummary(
}
const summaryTable = renderSummaryTable(buildResults)
const cachingReport = renderCachingReport(cacheReport, providerNote)
const hasFailure = anyFailed(buildResults)
if (config.shouldGenerateJobSummary(hasFailure)) {
core.info('Generating Job Summary')
+3 -3
View File
@@ -7,7 +7,7 @@ import * as jobSummary from './job-summary'
import * as buildScan from './develocity/build-scan'
import {loadBuildResults, markBuildResultsProcessed} from './build-results'
import {getCacheService} from './cache-service-loader'
import {getCacheService, getProviderNote} from './cache-service-loader'
import {CacheOptions} from './cache-service'
import {
DevelocityConfig,
@@ -65,8 +65,8 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
const gradleUserHome = core.getState(GRADLE_USER_HOME)
const cacheService = await getCacheService(cacheConfig)
const cachingReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
await jobSummary.generateJobSummary(buildResults, cachingReport, summaryConfig)
const cacheReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
await jobSummary.generateJobSummary(buildResults, cacheReport, getProviderNote(cacheConfig), summaryConfig)
markBuildResultsProcessed()
+11 -7
View File
@@ -138,8 +138,8 @@ describe('BasicCacheService', () => {
})
expect(mockSaveCache).not.toHaveBeenCalled()
expect(report).toContain('read-only')
expect(report).toContain(PRIMARY_KEY)
expect(report.status).toBe('read-only')
expect(report.entries[0].restoredKey).toBe(PRIMARY_KEY)
})
it('reports readOnly with no restore when cache was missed', async () => {
@@ -156,8 +156,9 @@ describe('BasicCacheService', () => {
})
expect(mockSaveCache).not.toHaveBeenCalled()
expect(report).toContain('read-only')
expect(report).toContain('No cache entry')
expect(report.status).toBe('read-only')
expect(report.entries[0].restoredKey).toBeUndefined()
expect(report.entries[0].restoredOutcome).toContain('not restored')
})
it('skips save when restored key equals primary key', async () => {
@@ -179,7 +180,8 @@ describe('BasicCacheService', () => {
})
expect(mockSaveCache).not.toHaveBeenCalled()
expect(report).toContain('Save was skipped')
expect(report.status).toBe('enabled')
expect(report.entries[0].savedOutcome).toContain('already exists')
})
it('saves cache and returns report on success', async () => {
@@ -204,7 +206,9 @@ describe('BasicCacheService', () => {
['/home/.gradle/caches', '/home/.gradle/wrapper'],
PRIMARY_KEY
)
expect(report).toContain('saved entry with key')
expect(report.status).toBe('enabled')
expect(report.entries[0].savedKey).toBe(PRIMARY_KEY)
expect(report.entries[0].savedOutcome).toBe('(Entry saved)')
})
it('warns on save failure instead of throwing', async () => {
@@ -228,7 +232,7 @@ describe('BasicCacheService', () => {
expect(mockWarning).toHaveBeenCalledWith(
expect.stringContaining('failed to save')
)
expect(report).toContain('failed')
expect(report.entries[0].savedOutcome).toContain('not saved')
})
})
})
+39 -10
View File
@@ -12,8 +12,7 @@ describe('getCacheService selection logic', () => {
const {getCacheService} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => true,
getCacheProvider: () => CacheProvider.Enhanced,
isCacheLicenseAccepted: () => true
getCacheProvider: () => CacheProvider.Enhanced
} as unknown as CacheConfig
const service = await getCacheService(mockConfig)
@@ -28,23 +27,53 @@ describe('getCacheService selection logic', () => {
excludes: []
})
// NoOpCacheService returns a specific report mentioning cache was disabled
expect(report).toContain('Cache was disabled')
// NoOpCacheService reports a disabled cache with no entries
expect(report.status).toBe('disabled')
expect(report.entries).toHaveLength(0)
})
it('wraps BasicCacheService with LicenseWarningCacheService when cache-provider is basic', async () => {
it('returns a BasicCacheService when cache-provider is basic', async () => {
const {getCacheService} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => false,
getCacheProvider: () => CacheProvider.Basic,
isCacheLicenseAccepted: () => false
getCacheProvider: () => CacheProvider.Basic
} as unknown as CacheConfig
const service = await getCacheService(mockConfig)
// The service should not be a bare BasicCacheService — it should be wrapped
// with LicenseWarningCacheService that appends the basic caching summary
const {BasicCacheService} = await import('../../src/cache-service-basic')
expect(service).not.toBeInstanceOf(BasicCacheService)
expect(service).toBeInstanceOf(BasicCacheService)
})
describe('getProviderNote', () => {
it('returns undefined when cache is disabled', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => true,
getCacheProvider: () => CacheProvider.Enhanced
} 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 = {
isCacheDisabled: () => false,
getCacheProvider: () => CacheProvider.Basic
} as unknown as CacheConfig
expect(getProviderNote(mockConfig)).toEqual({kind: 'basic'})
})
it('returns enhanced note for the enhanced provider', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => false,
getCacheProvider: () => CacheProvider.Enhanced
} as unknown as CacheConfig
expect(getProviderNote(mockConfig)).toEqual({kind: 'enhanced'})
})
})
})
+110
View File
@@ -0,0 +1,110 @@
import {describe, expect, it} from '@jest/globals'
import {CacheReport} from '../../src/cache-service'
import {renderCachingReport} from '../../src/caching-report'
const ENHANCED = {kind: 'enhanced'} as const
const BASIC = {kind: 'basic'} as const
function entry(overrides: Partial<CacheReport['entries'][number]> = {}): CacheReport['entries'][number] {
return {
entryName: 'Gradle User Home',
requestedKey: 'gradle-home-v1|key',
restoredKey: 'gradle-home-v1|key',
restoredSize: 535792,
restoredTime: 253,
restoredOutcome: '(Entry restored: exact match found)',
savedKey: 'gradle-home-v1|key',
savedSize: 528509,
savedTime: 257,
savedOutcome: '(Entry saved)',
...overrides
}
}
describe('renderCachingReport', () => {
it('renders an enhanced read-only report with heading, note and details', () => {
const report: CacheReport = {status: 'read-only', cleanup: 'disabled-readonly', entries: [entry()]}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('<h4>Gradle State Caching — ⚡ Enhanced (read-only)</h4>')
expect(md).toContain('[Enhanced Caching]')
expect(md).toContain('`gradle-actions-caching`')
expect(md).toContain('DISTRIBUTION.md')
expect(md).toContain('<details>')
expect(md).toContain('<summary>Entries: 1 restored, 1 saved (expand for more details)</summary>')
// status message moves inside the details expando
expect(md).toContain('Cache was read-only')
// read-only does not render the cleanup line
expect(md).not.toContain('Cache cleanup')
})
it('renders an enhanced enabled report with status and cleanup inside the details', () => {
const report: CacheReport = {status: 'enabled', cleanup: 'enabled', entries: [entry()]}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('<h4>Gradle State Caching — ⚡ Enhanced</h4>')
// status and cleanup messages are within the expando, after the summary
const detailsBody = md.slice(md.indexOf('</summary>'))
expect(detailsBody).toContain('Cache was enabled')
expect(detailsBody).toContain('Cache cleanup')
expect(md).toContain('<table>')
expect(md).toContain('Entries Restored')
})
it('renders a basic report with the upgrade note and no metrics table', () => {
const report: CacheReport = {
status: 'read-only',
entries: [
entry({
restoredSize: undefined,
restoredTime: undefined,
savedKey: undefined,
savedSize: undefined,
savedTime: undefined,
savedOutcome: '(Entry not saved: cache is read-only)'
})
]
}
const md = renderCachingReport(report, BASIC)
expect(md).toContain('<h4>Gradle State Caching — 🛡️ Basic (read-only)</h4>')
expect(md).toContain('[Basic Caching]')
expect(md).toContain('[Enhanced Caching]')
// DISTRIBUTION.md is only referenced for the enhanced provider
expect(md).not.toContain('DISTRIBUTION.md')
// No size/time data, so the metrics table is omitted but the entry list remains
expect(md).not.toContain('<table>')
expect(md).toContain('<pre>')
expect(md).toContain('<summary>Entries: 1 restored, 0 saved (expand for more details)</summary>')
})
it('renders a compact disabled report with no note and no details', () => {
const report: CacheReport = {status: 'disabled', entries: []}
const md = renderCachingReport(report, undefined)
expect(md).toContain('<h4>Gradle State Caching — Disabled</h4>')
expect(md).toContain('Caching was disabled')
expect(md).not.toContain('<details>')
expect(md).not.toContain('DISTRIBUTION.md')
})
it('renders a compact skipped report for a pre-existing Gradle User Home', () => {
const report: CacheReport = {status: 'disabled-existing-home', entries: []}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('<h4>Gradle State Caching — Skipped</h4>')
expect(md).toContain('pre-existing Gradle User Home')
expect(md).not.toContain('<details>')
// no provider note for non-active states
expect(md).not.toContain('DISTRIBUTION.md')
})
it('renders an unavailable report compactly', () => {
const report: CacheReport = {status: 'not-available', entries: []}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('<h4>Gradle State Caching — Unavailable</h4>')
expect(md).not.toContain('<details>')
})
})
+28 -1
View File
@@ -11,6 +11,23 @@ export declare interface BuildResult {
get buildScanFailed(): boolean;
}
/** @public */
export declare type CacheCleanupStatus = 'enabled' | 'disabled-param' | 'disabled-failure' | 'disabled-config-cache-hit' | 'disabled-readonly';
/** @public */
export declare interface CacheEntryReport {
entryName: string;
requestedKey?: string;
restoredKey?: string;
restoredSize?: number;
restoredTime?: number;
restoredOutcome: string;
savedKey?: string;
savedSize?: number;
savedTime?: number;
savedOutcome: string;
}
/** @public */
export declare interface CacheOptions {
disabled: boolean;
@@ -24,10 +41,20 @@ export declare interface CacheOptions {
excludes: string[];
}
/** @public */
export declare interface CacheReport {
status: CacheStatus;
cleanup?: CacheCleanupStatus;
entries: CacheEntryReport[];
}
/** @public */
export declare type CacheStatus = 'enabled' | 'read-only' | 'write-only' | 'disabled' | 'disabled-existing-home' | 'not-available';
/** @public */
export declare function restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>;
/** @public */
export declare function save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string>;
export declare function save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<CacheReport>;
export { }
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gradle-actions-caching",
"version": "0.6.0",
"version": "0.7.0",
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",