mirror of
https://github.com/gradle/actions.git
synced 2026-09-10 19:28:33 +08:00
This PR reports the support status of every Gradle version used in a workflow: - **End-of-life** — two or more major versions behind the latest release. - **Out of date** — one major version behind the latest release, or more than two minor versions behind on the current major. The information is surfaced as job annotations and in the Job Summary: an icon beside the Gradle version in the build results table, and a message below it. | version kind | job annotation | version table | below the table | | ------ | ----- | ---- | --------- | | EOL | warning | ⚠️ | expandable section, pointing to the Gradle Security Subscription | | Out of date | notice | ℹ️ | one-line legend, pointing to the Gradle release lifecycle docs | | Current | none | — | — | A single expandable section covers every end-of-life version, naming them in its summary line and listing the affected release lines in its body. The upgrade legend likewise appears once per job, however many versions carry the info icon. Notes on what is deliberately *not* reported: - **Patch releases.** Only the major and minor version are considered, so being on `9.7.0` when `9.7.1` exists is not flagged. - **Pre-releases.** Release candidates, milestones and snapshots are never reported, so testing against a nightly or an RC produces no annotations. The latest Gradle release is determined from the wrapper checksum data already bundled with the action, so no network access is required. That data is refreshed weekly, and the two-minor grace band absorbs the lag. Note that the annotations are emitted independently of `add-job-summary`: setting it to `never` suppresses the Job Summary itself, but the warning and notice annotations remain. ### Examples The `demo-job-summary` workflow has a `support-status-eol-and-outdated` job that builds with three end-of-life versions (`6.9.4`, `7.4`, `7.6.6`), two out-of-date versions (`8.0.2`, `8.14.5`) and the current release, so all three statuses appear in one summary: * **Job Summary**: https://github.com/gradle/actions/actions/runs/34166688755#summary-101879071306 * **Annotations**: https://github.com/gradle/actions/actions/runs/34166688755/job/101879071306 (expand annotations) That workflow is never triggered automatically; run it manually against a branch to review the rendering. ### Implementation * Adds `GradleVersion`, parsing and ordering versions the same way Gradle's own `org.gradle.util.GradleVersion` does. This replaces the previous `versionIsAtLeast` helper and removes the `semver` dependency. * Adds `gradle-support-status.ts`, which owns the classification policy and both of its presentations (annotations and Job Summary section) behind a three-function API, so `job-summary.ts` only asks for the icon and the rendered block. ### Testing * Unit tests for version ordering, classification, the annotations and the rendered summary. * `integ-test-gradle-support-status.yml`, which derives its expected versions from the bundled release data so the assertions cannot drift from the action's own view, then asserts against the real job log. * The demo workflow job linked above, for reviewing the rendering by eye. Documented under [Build reporting](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#gradle-version-support-status) in `docs/setup-gradle.md`. --------- Co-authored-by: Louis Jacomet <louis@gradle.com> Co-authored-by: Daz DeBoer <daz@gradle.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
302 lines
9.5 KiB
TypeScript
302 lines
9.5 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'
|
|
import {renderSupportStatus, supportStatusSign} from './gradle-support-status'
|
|
|
|
export async function generateJobSummary(
|
|
buildResults: BuildResult[],
|
|
cacheReport: CacheReport,
|
|
providerNote: ProviderNote | undefined,
|
|
config: SummaryConfig
|
|
): Promise<void> {
|
|
core.startGroup('Generating Job Summary')
|
|
|
|
const heading = renderActionHeading()
|
|
|
|
const errors = renderErrors()
|
|
if (errors) {
|
|
core.summary.addRaw(heading)
|
|
core.summary.addRaw(errors)
|
|
await core.summary.write()
|
|
return
|
|
}
|
|
|
|
const summaryTable = renderSummaryTable(buildResults)
|
|
const cachingReport = renderCachingReport(cacheReport, providerNote)
|
|
const hasFailure = anyFailed(buildResults)
|
|
|
|
core.info(summaryTable)
|
|
core.info('============================')
|
|
core.info(cachingReport)
|
|
|
|
if (config.shouldGenerateJobSummary(hasFailure)) {
|
|
core.summary.addRaw(heading)
|
|
core.summary.addRaw(summaryTable)
|
|
core.summary.addRaw(cachingReport)
|
|
await core.summary.write()
|
|
}
|
|
core.endGroup()
|
|
|
|
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)}
|
|
${renderActionHeading()}<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)}\n${renderSupportStatus(results.map(result => result.gradleVersion))}`
|
|
}
|
|
|
|
function renderActionHeading(): string {
|
|
const actionId = getActionId()
|
|
const caption = actionId ? ` <sub><em>captured by ${actionId}</em></sub>` : ''
|
|
return `<h3>Gradle Builds${caption}</h3>\n\n`
|
|
}
|
|
|
|
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'>${renderGradleVersion(result.gradleVersion)}</td>
|
|
<td align='center'>${renderOutcome(result)}</td>
|
|
<td>${renderBuildScan(result)}</td>
|
|
</tr>`
|
|
}
|
|
|
|
function renderGradleVersion(gradleVersion: string): string {
|
|
const sign = supportStatusSign(gradleVersion)
|
|
return sign ? `${gradleVersion} ${sign}` : gradleVersion
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|