mirror of
https://github.com/gradle/actions.git
synced 2026-06-12 14:40:39 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5082e2053e | |||
| e5857b8ffa | |||
| f5cb57ae79 | |||
| cbc6f4a545 | |||
| 483fcf682f | |||
| 40874b82b7 | |||
| a03a9c198b | |||
| 2577c609e7 |
@@ -4,7 +4,9 @@ import * as glob from '@actions/glob'
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
import {BuildResult} from './build-results'
|
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 PRIMARY_KEY_STATE = 'BASIC_CACHE_PRIMARY_KEY'
|
||||||
const RESTORED_KEY_STATE = 'BASIC_CACHE_RESTORED_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> {
|
async save(gradleUserHome: string, _buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<CacheReport> {
|
||||||
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.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const primaryKey = core.getState(PRIMARY_KEY_STATE)
|
const primaryKey = core.getState(PRIMARY_KEY_STATE)
|
||||||
const restoredKey = core.getState(RESTORED_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) {
|
if (restoredKey === primaryKey) {
|
||||||
core.info(`Basic caching restored entry with key \`${primaryKey}\`. Save was skipped.`)
|
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)
|
const cachePaths = getCachePaths(gradleUserHome)
|
||||||
@@ -62,14 +82,56 @@ export class BasicCacheService implements CacheService {
|
|||||||
try {
|
try {
|
||||||
await cache.saveCache(cachePaths, primaryKey)
|
await cache.saveCache(cachePaths, primaryKey)
|
||||||
core.info(`Basic caching saved entry with key: ${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) {
|
} catch (error) {
|
||||||
core.warning(`Basic caching failed to save entry with key \`${primaryKey}\`: ${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[] {
|
function getCachePaths(gradleUserHome: string): string[] {
|
||||||
return [path.join(gradleUserHome, 'caches'), path.join(gradleUserHome, 'wrapper')]
|
return [path.join(gradleUserHome, 'caches'), path.join(gradleUserHome, 'wrapper')]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,57 +5,24 @@ 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, CacheService} from './cache-service'
|
import {CacheOptions, CacheReport, CacheService} from './cache-service'
|
||||||
|
import {ProviderNote} from './caching-report'
|
||||||
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.
|
|
||||||
`
|
|
||||||
|
|
||||||
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 ENHANCED_CACHE_SUMMARY = `
|
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.`
|
||||||
> [!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.`
|
|
||||||
|
|
||||||
class NoOpCacheService implements CacheService {
|
class NoOpCacheService implements CacheService {
|
||||||
async restore(_gradleUserHome: string, _cacheOptions: CacheOptions): Promise<void> {
|
async restore(_gradleUserHome: string, _cacheOptions: CacheOptions): Promise<void> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(_gradleUserHome: string, _buildResults: BuildResult[], _cacheOptions: CacheOptions): Promise<string> {
|
async save(
|
||||||
return NOOP_CACHING_REPORT
|
_gradleUserHome: string,
|
||||||
}
|
_buildResults: BuildResult[],
|
||||||
}
|
_cacheOptions: CacheOptions
|
||||||
|
): Promise<CacheReport> {
|
||||||
class LicenseWarningCacheService implements CacheService {
|
return {status: 'disabled', entries: []}
|
||||||
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}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,16 +34,22 @@ export async function getCacheService(cacheConfig: CacheConfig): Promise<CacheSe
|
|||||||
|
|
||||||
if (cacheConfig.getCacheProvider() === CacheProvider.Basic) {
|
if (cacheConfig.getCacheProvider() === CacheProvider.Basic) {
|
||||||
logCacheMessage(BASIC_CACHE_MESSAGE)
|
logCacheMessage(BASIC_CACHE_MESSAGE)
|
||||||
return new LicenseWarningCacheService(new BasicCacheService(), BASIC_CACHE_SUMMARY)
|
return new BasicCacheService()
|
||||||
}
|
}
|
||||||
|
|
||||||
logCacheMessage(ENHANCED_CACHE_MESSAGE)
|
logCacheMessage(ENHANCED_CACHE_MESSAGE)
|
||||||
const cacheService = await loadVendoredCacheService()
|
return loadVendoredCacheService()
|
||||||
if (cacheConfig.isCacheLicenseAccepted()) {
|
}
|
||||||
return cacheService
|
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
export async function loadVendoredCacheService(): Promise<CacheService> {
|
||||||
|
|||||||
@@ -12,7 +12,45 @@ export interface CacheOptions {
|
|||||||
excludes: string[]
|
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 {
|
export interface CacheService {
|
||||||
restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>
|
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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`
|
||||||
|
}
|
||||||
@@ -167,11 +167,6 @@ export class CacheConfig {
|
|||||||
return core.getMultilineInput('gradle-home-cache-excludes')
|
return core.getMultilineInput('gradle-home-cache-excludes')
|
||||||
}
|
}
|
||||||
|
|
||||||
isCacheLicenseAccepted(): boolean {
|
|
||||||
const dvConfig = new DevelocityConfig()
|
|
||||||
return dvConfig.getDevelocityAccessKey() !== '' || dvConfig.hasTermsOfUseAgreement()
|
|
||||||
}
|
|
||||||
|
|
||||||
getCacheProvider(): CacheProvider {
|
getCacheProvider(): CacheProvider {
|
||||||
const val = core.getInput('cache-provider')
|
const val = core.getInput('cache-provider')
|
||||||
switch (val.toLowerCase().trim()) {
|
switch (val.toLowerCase().trim()) {
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import * as core from '@actions/core'
|
|||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
|
|
||||||
import {BuildResult} from './build-results'
|
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 {DependencyGraphConfig, getActionId, getGithubToken, getJobMatrix, SummaryConfig} from './configuration'
|
||||||
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
|
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
|
||||||
|
|
||||||
export async function generateJobSummary(
|
export async function generateJobSummary(
|
||||||
buildResults: BuildResult[],
|
buildResults: BuildResult[],
|
||||||
cachingReport: string,
|
cacheReport: CacheReport,
|
||||||
|
providerNote: ProviderNote | undefined,
|
||||||
config: SummaryConfig
|
config: SummaryConfig
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const errors = renderErrors()
|
const errors = renderErrors()
|
||||||
@@ -18,6 +21,7 @@ export async function generateJobSummary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const summaryTable = renderSummaryTable(buildResults)
|
const summaryTable = renderSummaryTable(buildResults)
|
||||||
|
const cachingReport = renderCachingReport(cacheReport, providerNote)
|
||||||
const hasFailure = anyFailed(buildResults)
|
const hasFailure = anyFailed(buildResults)
|
||||||
if (config.shouldGenerateJobSummary(hasFailure)) {
|
if (config.shouldGenerateJobSummary(hasFailure)) {
|
||||||
core.info('Generating Job Summary')
|
core.info('Generating Job Summary')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as jobSummary from './job-summary'
|
|||||||
import * as buildScan from './develocity/build-scan'
|
import * as buildScan from './develocity/build-scan'
|
||||||
|
|
||||||
import {loadBuildResults, markBuildResultsProcessed} from './build-results'
|
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 {CacheOptions} from './cache-service'
|
||||||
import {
|
import {
|
||||||
DevelocityConfig,
|
DevelocityConfig,
|
||||||
@@ -65,8 +65,8 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
|
|||||||
|
|
||||||
const gradleUserHome = core.getState(GRADLE_USER_HOME)
|
const gradleUserHome = core.getState(GRADLE_USER_HOME)
|
||||||
const cacheService = await getCacheService(cacheConfig)
|
const cacheService = await getCacheService(cacheConfig)
|
||||||
const cachingReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
|
const cacheReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
|
||||||
await jobSummary.generateJobSummary(buildResults, cachingReport, summaryConfig)
|
await jobSummary.generateJobSummary(buildResults, cacheReport, getProviderNote(cacheConfig), summaryConfig)
|
||||||
|
|
||||||
markBuildResultsProcessed()
|
markBuildResultsProcessed()
|
||||||
|
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ describe('BasicCacheService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(mockSaveCache).not.toHaveBeenCalled()
|
expect(mockSaveCache).not.toHaveBeenCalled()
|
||||||
expect(report).toContain('read-only')
|
expect(report.status).toBe('read-only')
|
||||||
expect(report).toContain(PRIMARY_KEY)
|
expect(report.entries[0].restoredKey).toBe(PRIMARY_KEY)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reports readOnly with no restore when cache was missed', async () => {
|
it('reports readOnly with no restore when cache was missed', async () => {
|
||||||
@@ -156,8 +156,9 @@ describe('BasicCacheService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(mockSaveCache).not.toHaveBeenCalled()
|
expect(mockSaveCache).not.toHaveBeenCalled()
|
||||||
expect(report).toContain('read-only')
|
expect(report.status).toBe('read-only')
|
||||||
expect(report).toContain('No cache entry')
|
expect(report.entries[0].restoredKey).toBeUndefined()
|
||||||
|
expect(report.entries[0].restoredOutcome).toContain('not restored')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('skips save when restored key equals primary key', async () => {
|
it('skips save when restored key equals primary key', async () => {
|
||||||
@@ -179,7 +180,8 @@ describe('BasicCacheService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(mockSaveCache).not.toHaveBeenCalled()
|
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 () => {
|
it('saves cache and returns report on success', async () => {
|
||||||
@@ -204,7 +206,9 @@ describe('BasicCacheService', () => {
|
|||||||
['/home/.gradle/caches', '/home/.gradle/wrapper'],
|
['/home/.gradle/caches', '/home/.gradle/wrapper'],
|
||||||
PRIMARY_KEY
|
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 () => {
|
it('warns on save failure instead of throwing', async () => {
|
||||||
@@ -228,7 +232,7 @@ describe('BasicCacheService', () => {
|
|||||||
expect(mockWarning).toHaveBeenCalledWith(
|
expect(mockWarning).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('failed to save')
|
expect.stringContaining('failed to save')
|
||||||
)
|
)
|
||||||
expect(report).toContain('failed')
|
expect(report.entries[0].savedOutcome).toContain('not saved')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ describe('getCacheService selection logic', () => {
|
|||||||
const {getCacheService} = await import('../../src/cache-service-loader')
|
const {getCacheService} = await import('../../src/cache-service-loader')
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isCacheDisabled: () => true,
|
isCacheDisabled: () => true,
|
||||||
getCacheProvider: () => CacheProvider.Enhanced,
|
getCacheProvider: () => CacheProvider.Enhanced
|
||||||
isCacheLicenseAccepted: () => true
|
|
||||||
} as unknown as CacheConfig
|
} as unknown as CacheConfig
|
||||||
|
|
||||||
const service = await getCacheService(mockConfig)
|
const service = await getCacheService(mockConfig)
|
||||||
@@ -28,23 +27,53 @@ describe('getCacheService selection logic', () => {
|
|||||||
excludes: []
|
excludes: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// NoOpCacheService returns a specific report mentioning cache was disabled
|
// NoOpCacheService reports a disabled cache with no entries
|
||||||
expect(report).toContain('Cache was disabled')
|
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 {getCacheService} = await import('../../src/cache-service-loader')
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isCacheDisabled: () => false,
|
isCacheDisabled: () => false,
|
||||||
getCacheProvider: () => CacheProvider.Basic,
|
getCacheProvider: () => CacheProvider.Basic
|
||||||
isCacheLicenseAccepted: () => false
|
|
||||||
} as unknown as CacheConfig
|
} as unknown as CacheConfig
|
||||||
|
|
||||||
const service = await getCacheService(mockConfig)
|
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')
|
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'})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -11,6 +11,23 @@ export declare interface BuildResult {
|
|||||||
get buildScanFailed(): boolean;
|
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 */
|
/** @public */
|
||||||
export declare interface CacheOptions {
|
export declare interface CacheOptions {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@@ -24,10 +41,20 @@ export declare interface CacheOptions {
|
|||||||
excludes: string[];
|
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 */
|
/** @public */
|
||||||
export declare function restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>;
|
export declare function restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>;
|
||||||
|
|
||||||
/** @public */
|
/** @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 { }
|
export { }
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradle-actions-caching",
|
"name": "gradle-actions-caching",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"types": "./index.d.ts",
|
"types": "./index.d.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user