mirror of
https://github.com/gradle/actions.git
synced 2026-07-03 09:20:51 +00:00
d97d29c992
The not-registered job-summary link and the core.notice pointed at the bare /register URL, so the page replied 'owner and repo are required'. Append ?owner=&repo= from GITHUB_REPOSITORY so the backend can resolve the installation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
221 lines
11 KiB
TypeScript
221 lines
11 KiB
TypeScript
import {CacheCleanupStatus, CacheEntryReport, CacheReport, CacheStatus, ProjectCacheStatus} 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'
|
|
const REGISTER_BASE = 'https://actions-caching-registration.vercel.app/register'
|
|
|
|
/**
|
|
* The `/register` link with the current repo's identity appended, so the backend can resolve the
|
|
* installation and route to acceptance. Repo identity comes from `GITHUB_REPOSITORY` (`owner/repo`),
|
|
* set by the runner; falls back to the bare link if it is somehow unavailable.
|
|
*/
|
|
function registerUrl(): string {
|
|
const repository = process.env.GITHUB_REPOSITORY
|
|
if (!repository?.includes('/')) {
|
|
return REGISTER_BASE
|
|
}
|
|
const [owner, repo] = repository.split('/')
|
|
return `${REGISTER_BASE}?${new URLSearchParams({owner, repo}).toString()}`
|
|
}
|
|
|
|
/**
|
|
* 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.`
|
|
}
|
|
|
|
// 'not-registered' is rendered dynamically (see renderProjectCacheLine) because its /register link
|
|
// carries the repo identity, so it is intentionally absent from this static map.
|
|
const PROJECT_CACHE_COPY: Record<Exclude<ProjectCacheStatus, 'not-registered'>, string> = {
|
|
'not-enabled': ``,
|
|
'trial-expired': `Project state (build-logic and configuration cache) was not cached - the Develocity caching trial has expired.`,
|
|
'no-encryption-key': `Project state (build-logic and configuration cache) was not cached - a [cache-encryption-key](${DOCS}#cache-encryption-key) is required.`,
|
|
enabled: `Caching of project state (build-logic and configuration cache) was enabled.`
|
|
}
|
|
|
|
/**
|
|
* 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 renderProjectCacheLine(projectCache?: ProjectCacheStatus): string | undefined {
|
|
if (!projectCache) {
|
|
return undefined
|
|
}
|
|
if (projectCache === 'not-registered') {
|
|
return `Project state (build-logic and configuration cache) was not cached - this repository is not registered for advanced caching. [Register this repository](${registerUrl()}), or provide a \`develocity-access-key\` and \`develocity-server-url\`.`
|
|
}
|
|
// PROJECT_CACHE_COPY['not-enabled'] is '', which the .filter(Boolean) at the call site drops.
|
|
return PROJECT_CACHE_COPY[projectCache]
|
|
}
|
|
|
|
/**
|
|
* A plain-text log notice (no markdown) surfaced when advanced caching was withheld because the
|
|
* repository is not registered. Returns `undefined` for every other status, so callers stay quiet
|
|
* when the feature is enabled, disabled, or simply not opted in.
|
|
*/
|
|
export function renderProjectCacheNotice(projectCache?: ProjectCacheStatus): string | undefined {
|
|
if (projectCache !== 'not-registered') {
|
|
return undefined
|
|
}
|
|
return (
|
|
'Advanced caching (build-logic and configuration cache) was not enabled: this repository ' +
|
|
`is not registered. Register it at ${registerUrl()}, or provide a develocity-access-key and ` +
|
|
'develocity-server-url.'
|
|
)
|
|
}
|
|
|
|
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 projectCache = renderProjectCacheLine(report.projectCache)
|
|
const table = renderEntryTable(report.entries)
|
|
const pre = `<pre>\n${renderEntryDetails(report.entries)}</pre>`
|
|
const body = [STATUS_COPY[report.status], cleanup, projectCache, 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`
|
|
}
|