Redesign the caching Job Summary (#985)

Redesigns the caching section of the Job Summary into a single,
consistent layout across every cache provider and state, and integrates
the provider message into the report rather than appending it
disconnected at the bottom.

## Motivation

The caching report was produced by three divergent code paths (NoOp /
basic / enhanced), each rendering its own markdown:

- **Explicitly disabled** → a one-line message, no expand, no provider
note.
- **Enhanced** (incl. skipped-due-to-existing-home) → a full `<details>`
block.
- **Basic** → a one-line message with **no** expandable details at all.

The Enhanced/Basic provider note floated at the very bottom,
disconnected from the report.

## What changed

`save()` now returns structured `CacheReport` data instead of
pre-rendered HTML, and a single renderer (`caching-report.ts`) produces
one unified layout for all variants:

- **Section heading**: `#### <icon> Gradle Caching — <Provider>
(<status>)`
- **Status line** explaining what the cache did
- **Integrated provider note** woven in under the heading — now shown
**unconditionally** (no longer gated on license acceptance)
- **Expandable cache-entry details** when there are entries — basic
caching now gets this too

The two disabled variants (explicitly disabled, and skipped due to a
pre-existing Gradle User Home) render as **compact callouts with no
expandable section**.

### Main repo
- `caching-report.ts` (new): central renderer + all framing copy + entry
table/`<pre>` helpers.
- `cache-service.ts`: `CacheReport` / `CacheEntryReport` / status types;
`save()` returns `CacheReport`.
- `cache-service-loader.ts`: `NoOp` returns a report;
`LicenseWarningCacheService` removed; new `getProviderNote()`.
- `cache-service-basic.ts`: builds a `CacheReport`.
- `job-summary.ts` / `setup-gradle.ts`: thread `CacheReport` +
`ProviderNote`.
- `configuration.ts`: remove now-unused `isCacheLicenseAccepted()`.

### Vendored library
The structured contract requires **gradle-actions-caching v0.7.0**
(gradle/actions-caching#74). This PR updates the vendored library to
that release — the official `Update gradle-actions-caching library to
v0.7.0` vendor commit is included here, so merging this PR ships the
redesign together with the library it depends on.

## Testing

- Both repos build; prettier + eslint clean.
- `gradle/actions`: 363/363 Jest tests pass, including new
`caching-report.test.ts` covering every variant.
- `gradle-actions-caching`: 74/74 pass under JDK 17.
- Rendered markdown verified for all five variants (enhanced/basic
enabled & read-only, disabled, skipped).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Bot Githubaction <bot-githubaction@gradle.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daz DeBoer
2026-06-12 15:21:52 +00:00
committed by GitHub
co-authored by Bot Githubaction Claude Opus 4.8
parent 8b6cdb5f58
commit 97715a29bc
13 changed files with 502 additions and 92 deletions
+168
View File
@@ -0,0 +1,168 @@
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 caching provider. For faster builds and advanced features, consider the **[Enhanced Caching](${DOCS}#enhanced-caching)** provider.`
}
function renderDetails(report: CacheReport): string {
const entries = report.entries
const restored = entries.filter(entry => entry.restoredKey).length
const saved = entries.filter(entry => entry.savedKey).length
const summary = hasMetrics(entries)
? `Entries: ${restored} restored (${getSize(entries, e => e.restoredSize)}Mb), ${saved} saved (${getSize(entries, e => e.savedSize)}Mb) - Expand for more details`
: `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 hasMetrics(entries: CacheEntryReport[]): boolean {
return entries.some(entry => entry.restoredSize || entry.restoredTime || entry.savedSize || entry.savedTime)
}
function renderEntryTable(entries: CacheEntryReport[]): string {
if (!hasMetrics(entries)) {
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`
}