mirror of
https://github.com/gradle/actions.git
synced 2026-06-13 15:10:38 +00:00
97715a29bc
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>
239 lines
8.5 KiB
TypeScript
239 lines
8.5 KiB
TypeScript
import {describe, expect, it, jest, beforeEach} from '@jest/globals'
|
|
|
|
// Mock @actions/cache
|
|
const mockRestoreCache = jest.fn<(paths: string[], primaryKey: string, restoreKeys?: string[]) => Promise<string | undefined>>()
|
|
const mockSaveCache = jest.fn<(paths: string[], key: string) => Promise<number>>()
|
|
jest.unstable_mockModule('@actions/cache', () => ({
|
|
restoreCache: mockRestoreCache,
|
|
saveCache: mockSaveCache
|
|
}))
|
|
|
|
// Mock @actions/core
|
|
const mockInfo = jest.fn<(message: string) => void>()
|
|
const mockWarning = jest.fn<(message: string) => void>()
|
|
const mockSaveState = jest.fn<(name: string, value: string) => void>()
|
|
const mockGetState = jest.fn<(name: string) => string>()
|
|
jest.unstable_mockModule('@actions/core', () => ({
|
|
info: mockInfo,
|
|
warning: mockWarning,
|
|
saveState: mockSaveState,
|
|
getState: mockGetState
|
|
}))
|
|
|
|
// Mock @actions/glob
|
|
const mockHashFiles = jest.fn<(pattern: string) => Promise<string>>()
|
|
jest.unstable_mockModule('@actions/glob', () => ({
|
|
hashFiles: mockHashFiles
|
|
}))
|
|
|
|
const {BasicCacheService} = await import('../../src/cache-service-basic')
|
|
|
|
const HASH = 'abc123def456'
|
|
const PRIMARY_KEY = `setup-java-Linux-${process.arch}-gradle-${HASH}`
|
|
|
|
describe('BasicCacheService', () => {
|
|
let service: InstanceType<typeof BasicCacheService>
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
service = new BasicCacheService()
|
|
process.env['RUNNER_OS'] = 'Linux'
|
|
mockHashFiles.mockResolvedValue(HASH)
|
|
})
|
|
|
|
describe('restore', () => {
|
|
it('restores cache without restoreKeys and saves both keys to state', async () => {
|
|
mockRestoreCache.mockResolvedValue(PRIMARY_KEY)
|
|
|
|
await service.restore('/home/.gradle', {
|
|
disabled: false,
|
|
readOnly: false,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
|
|
// No restoreKeys parameter — exact match only (setup-java#269)
|
|
expect(mockRestoreCache).toHaveBeenCalledWith(
|
|
['/home/.gradle/caches', '/home/.gradle/wrapper'],
|
|
PRIMARY_KEY
|
|
)
|
|
expect(mockSaveState).toHaveBeenCalledWith('BASIC_CACHE_PRIMARY_KEY', PRIMARY_KEY)
|
|
expect(mockSaveState).toHaveBeenCalledWith('BASIC_CACHE_RESTORED_KEY', PRIMARY_KEY)
|
|
})
|
|
|
|
it('saves primary key to state even on cache miss', async () => {
|
|
mockRestoreCache.mockResolvedValue(undefined)
|
|
|
|
await service.restore('/home/.gradle', {
|
|
disabled: false,
|
|
readOnly: false,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
|
|
expect(mockSaveState).toHaveBeenCalledWith('BASIC_CACHE_PRIMARY_KEY', PRIMARY_KEY)
|
|
expect(mockSaveState).not.toHaveBeenCalledWith('BASIC_CACHE_RESTORED_KEY', expect.anything())
|
|
expect(mockInfo).toHaveBeenCalledWith(
|
|
expect.stringContaining('did not find')
|
|
)
|
|
})
|
|
|
|
it('warns on restore failure instead of throwing', async () => {
|
|
mockRestoreCache.mockRejectedValue(new Error('Network error'))
|
|
|
|
await service.restore('/home/.gradle', {
|
|
disabled: false,
|
|
readOnly: false,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
|
|
expect(mockWarning).toHaveBeenCalledWith(
|
|
expect.stringContaining('failed to restore')
|
|
)
|
|
})
|
|
|
|
it('throws when no build files are found', async () => {
|
|
mockHashFiles.mockResolvedValue('')
|
|
|
|
await expect(
|
|
service.restore('/home/.gradle', {
|
|
disabled: false,
|
|
readOnly: false,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
).rejects.toThrow('No file in')
|
|
})
|
|
})
|
|
|
|
describe('save', () => {
|
|
it('reports readOnly with restored key when cache was hit', async () => {
|
|
mockGetState.mockReturnValue(PRIMARY_KEY)
|
|
const report = await service.save('/home/.gradle', [], {
|
|
disabled: false,
|
|
readOnly: true,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
|
|
expect(mockSaveCache).not.toHaveBeenCalled()
|
|
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 () => {
|
|
mockGetState.mockReturnValue('')
|
|
const report = await service.save('/home/.gradle', [], {
|
|
disabled: false,
|
|
readOnly: true,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
|
|
expect(mockSaveCache).not.toHaveBeenCalled()
|
|
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 () => {
|
|
mockGetState.mockImplementation((name: string) => {
|
|
if (name === 'BASIC_CACHE_PRIMARY_KEY') return PRIMARY_KEY
|
|
if (name === 'BASIC_CACHE_RESTORED_KEY') return PRIMARY_KEY
|
|
return ''
|
|
})
|
|
|
|
const report = await service.save('/home/.gradle', [], {
|
|
disabled: false,
|
|
readOnly: false,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
|
|
expect(mockSaveCache).not.toHaveBeenCalled()
|
|
expect(report.status).toBe('enabled')
|
|
expect(report.entries[0].savedOutcome).toContain('already exists')
|
|
})
|
|
|
|
it('saves cache and returns report on success', async () => {
|
|
mockGetState.mockImplementation((name: string) => {
|
|
if (name === 'BASIC_CACHE_PRIMARY_KEY') return PRIMARY_KEY
|
|
return ''
|
|
})
|
|
mockSaveCache.mockResolvedValue(0)
|
|
|
|
const report = await service.save('/home/.gradle', [], {
|
|
disabled: false,
|
|
readOnly: false,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
|
|
expect(mockSaveCache).toHaveBeenCalledWith(
|
|
['/home/.gradle/caches', '/home/.gradle/wrapper'],
|
|
PRIMARY_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 () => {
|
|
mockGetState.mockImplementation((name: string) => {
|
|
if (name === 'BASIC_CACHE_PRIMARY_KEY') return PRIMARY_KEY
|
|
return ''
|
|
})
|
|
mockSaveCache.mockRejectedValue(new Error('Storage full'))
|
|
|
|
const report = await service.save('/home/.gradle', [], {
|
|
disabled: false,
|
|
readOnly: false,
|
|
writeOnly: false,
|
|
overwriteExisting: false,
|
|
strictMatch: false,
|
|
cleanup: 'never',
|
|
includes: [],
|
|
excludes: []
|
|
})
|
|
|
|
expect(mockWarning).toHaveBeenCalledWith(
|
|
expect.stringContaining('failed to save')
|
|
)
|
|
expect(report.entries[0].savedOutcome).toContain('not saved')
|
|
})
|
|
})
|
|
})
|