mirror of
https://github.com/gradle/actions.git
synced 2026-07-28 07:04:32 +02:00
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>
288 lines
8.9 KiB
TypeScript
288 lines
8.9 KiB
TypeScript
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[],
|
|
cacheReport: CacheReport,
|
|
providerNote: ProviderNote | undefined,
|
|
config: SummaryConfig
|
|
): Promise<void> {
|
|
const errors = renderErrors()
|
|
if (errors) {
|
|
core.summary.addRaw(errors)
|
|
await core.summary.write()
|
|
return
|
|
}
|
|
|
|
const summaryTable = renderSummaryTable(buildResults)
|
|
const cachingReport = renderCachingReport(cacheReport, providerNote)
|
|
const hasFailure = anyFailed(buildResults)
|
|
if (config.shouldGenerateJobSummary(hasFailure)) {
|
|
core.info('Generating Job Summary')
|
|
|
|
core.summary.addRaw(summaryTable)
|
|
core.summary.addRaw(cachingReport)
|
|
await core.summary.write()
|
|
} else {
|
|
core.info('============================')
|
|
core.info(summaryTable)
|
|
core.info('============================')
|
|
core.info(cachingReport)
|
|
core.info('============================')
|
|
}
|
|
|
|
if (config.canAddPRComment()) {
|
|
await minimizeObsoletePRComments()
|
|
}
|
|
|
|
if (config.shouldAddPRComment(hasFailure)) {
|
|
await addPRComment(summaryTable)
|
|
}
|
|
}
|
|
|
|
async function addPRComment(jobSummary: string): Promise<void> {
|
|
const context = github.context
|
|
if (context.payload.pull_request == null) {
|
|
core.info('No pull_request trigger detected: not adding PR comment')
|
|
return
|
|
}
|
|
|
|
const pull_request_number = context.payload.pull_request.number
|
|
core.info(`Adding Job Summary as comment to PR #${pull_request_number}.`)
|
|
|
|
const prComment = `${jobMarker(context)}
|
|
<h3>Job Summary for Gradle</h3>
|
|
<a href="${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}" target="_blank">
|
|
<h5>${context.workflow} :: <em>${context.job}</em></h5>
|
|
</a>
|
|
|
|
${jobSummary}`
|
|
|
|
const github_token = getGithubToken()
|
|
const octokit = github.getOctokit(github_token)
|
|
|
|
try {
|
|
await octokit.rest.issues.createComment({
|
|
...context.repo,
|
|
issue_number: pull_request_number,
|
|
body: prComment
|
|
})
|
|
} catch (error) {
|
|
if (error instanceof Error && error.name === 'HttpError') {
|
|
core.warning(buildWarningMessage(error))
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildWarningMessage(error: Error): string {
|
|
const mainWarning = `Failed to generate PR comment.\n${String(error)}`
|
|
if (error.message === 'Resource not accessible by integration') {
|
|
return `${mainWarning}
|
|
Please ensure that the 'pull-requests: write' permission is available for the workflow job.
|
|
Note that this permission is never available for a workflow triggered from a repository fork.
|
|
`
|
|
}
|
|
return mainWarning
|
|
}
|
|
|
|
export function renderSummaryTable(results: BuildResult[]): string {
|
|
return `${renderDeprecations()}\n${renderBuildResults(results)}`
|
|
}
|
|
|
|
function renderErrors(): string | undefined {
|
|
const errors = getErrors()
|
|
if (errors.length === 0) {
|
|
return undefined
|
|
}
|
|
return errors.map(error => `<b>:x: ${error}</b>`).join('\n')
|
|
}
|
|
|
|
function renderDeprecations(): string {
|
|
const deprecations = getDeprecations()
|
|
if (deprecations.length === 0) {
|
|
return ''
|
|
}
|
|
return `
|
|
<h4>Deprecation warnings</h4>
|
|
This job uses deprecated functionality from the <code>${getActionId()}</code> action. Follow the links for upgrade details.
|
|
<ul>
|
|
${deprecations.map(deprecation => `<li>${getDeprecationHtml(deprecation)}</li>`).join('')}
|
|
</ul>
|
|
|
|
<h4>Gradle Build Results</h4>`
|
|
}
|
|
|
|
function getDeprecationHtml(deprecation: Deprecation): string {
|
|
return `<a href="${deprecation.getDocumentationLink()}" target="_blank">${deprecation.message}</a>`
|
|
}
|
|
|
|
function renderBuildResults(results: BuildResult[]): string {
|
|
if (results.length === 0) {
|
|
return '<b>No Gradle build results detected.</b>'
|
|
}
|
|
|
|
return `
|
|
<table>
|
|
<tr>
|
|
<th>Gradle Root Project</th>
|
|
<th>Requested Tasks</th>
|
|
<th>Gradle Version</th>
|
|
<th>Build Outcome</th>
|
|
<th>Build Scan®</th>
|
|
</tr>${results.map(result => renderBuildResultRow(result)).join('')}
|
|
</table>
|
|
`
|
|
}
|
|
|
|
function anyFailed(results: BuildResult[]): boolean {
|
|
return results.some(result => result.buildFailed)
|
|
}
|
|
|
|
function renderBuildResultRow(result: BuildResult): string {
|
|
return `
|
|
<tr>
|
|
<td>${truncateString(result.rootProjectName, 30)}</td>
|
|
<td>${truncateString(result.requestedTasks, 60)}</td>
|
|
<td align='center'>${result.gradleVersion}</td>
|
|
<td align='center'>${renderOutcome(result)}</td>
|
|
<td>${renderBuildScan(result)}</td>
|
|
</tr>`
|
|
}
|
|
|
|
function renderOutcome(result: BuildResult): string {
|
|
return result.buildFailed ? ':x:' : ':white_check_mark:'
|
|
}
|
|
|
|
interface BadgeSpec {
|
|
text: string
|
|
alt: string
|
|
color: string
|
|
logo: boolean
|
|
targetUrl: string
|
|
}
|
|
|
|
function renderBuildScan(result: BuildResult): string {
|
|
if (result.buildScanFailed) {
|
|
return renderBuildScanBadge({
|
|
text: 'Publish failed',
|
|
alt: 'Build Scan publish failed',
|
|
color: 'orange',
|
|
logo: false,
|
|
targetUrl: 'https://docs.gradle.com/develocity/gradle-plugin/#troubleshooting'
|
|
})
|
|
}
|
|
if (result.buildScanUri) {
|
|
return renderBuildScanBadge({
|
|
text: 'Build Scan®',
|
|
alt: 'Build Scan published',
|
|
color: '06A0CE',
|
|
logo: true,
|
|
targetUrl: result.buildScanUri
|
|
})
|
|
}
|
|
return renderBuildScanBadge({
|
|
text: 'Not published',
|
|
alt: 'Build Scan not published',
|
|
color: 'lightgrey',
|
|
logo: false,
|
|
targetUrl: 'https://scans.gradle.com'
|
|
})
|
|
}
|
|
|
|
function renderBuildScanBadge({text, alt, color, logo, targetUrl}: BadgeSpec): string {
|
|
const encodedText = encodeURIComponent(text)
|
|
const badgeUrl = `https://img.shields.io/badge/${encodedText}-${color}${logo ? '?logo=Gradle' : ''}`
|
|
const badgeHtml = `<img src="${badgeUrl}" alt="${alt}" />`
|
|
return `<a href="${targetUrl}" rel="nofollow" target="_blank">${badgeHtml}</a>`
|
|
}
|
|
|
|
function truncateString(str: string, maxLength: number): string {
|
|
if (str.length > maxLength) {
|
|
return `<div title='${str}'>${str.slice(0, maxLength - 1)}…</div>`
|
|
} else {
|
|
return str
|
|
}
|
|
}
|
|
|
|
async function minimizeObsoletePRComments(): Promise<void> {
|
|
const context = github.context
|
|
if (context.payload.pull_request == null) {
|
|
core.info('No pull_request trigger detected: not minimizing obsolete PR comments')
|
|
return
|
|
}
|
|
|
|
const prNumber = context.payload.pull_request.number
|
|
core.info(`Minimizing obsolete Job Summary comments on PR #${prNumber}.`)
|
|
|
|
const marker = jobMarker(context)
|
|
const octokit = github.getOctokit(getGithubToken())
|
|
const {owner, repo} = context.repo
|
|
|
|
const query = `
|
|
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
repository(owner: $owner, name: $repo) {
|
|
pullRequest(number: $prNumber) {
|
|
comments(last: 100) {
|
|
nodes { id body isMinimized url }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
let comments: PullRequestComment[]
|
|
try {
|
|
const {repository} = await octokit.graphql<CommentsQueryResult>(query, {owner, repo, prNumber})
|
|
comments = repository.pullRequest?.comments?.nodes?.filter((c): c is PullRequestComment => c !== null) ?? []
|
|
} catch (error) {
|
|
return core.warning(`Failed to fetch comments: ${error}`)
|
|
}
|
|
|
|
const mutation = `
|
|
mutation($id: ID!) {
|
|
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
|
|
clientMutationId
|
|
}
|
|
}
|
|
`
|
|
|
|
const commentsToMinimize = comments
|
|
.filter(c => !c.isMinimized && c.body.includes(marker))
|
|
.map(async c =>
|
|
octokit
|
|
.graphql(mutation, {id: c.id})
|
|
.then(() => core.info(`Successfully minimized (id:${c.id}, url:${c.url})`))
|
|
.catch(e => core.warning(`Failed to minimize (id:${c.id}, url:${c.url}, error:${e?.message || e})`))
|
|
)
|
|
await Promise.allSettled(commentsToMinimize)
|
|
}
|
|
|
|
export function jobMarker(context: typeof github.context): string {
|
|
const jobCorrelator = DependencyGraphConfig.constructJobCorrelator(context.workflow, context.job, getJobMatrix())
|
|
return `<!-- gradle-job-summary: ${jobCorrelator} -->`
|
|
}
|
|
|
|
interface PullRequestComment {
|
|
id: string
|
|
body: string
|
|
isMinimized: boolean
|
|
url: string
|
|
}
|
|
|
|
interface CommentsQueryResult {
|
|
repository: {
|
|
pullRequest?: {
|
|
comments?: {
|
|
nodes?: (PullRequestComment | null)[] | null
|
|
} | null
|
|
} | null
|
|
}
|
|
}
|