Files
gradle/sources/src/job-summary.ts
T
318eed7038 Hide obsolete Job summaries (#902)
- Injects a `<!-- gradle-job-summary: ${jobCorrelator} -->` marker on
each job summary
- Lists 100 last comments: unfortunately there is no API to specifically
filter for comments, and checking the last 100 comments (the limit) is
usually enough and does not require iterating over pages
- Mutate comments having this expected marker

I tried to add some tests, but I'm not familiar enough to setup a
complete test suite with proper mocking of GitHub/Octokit with jest.

I could potentially extract the `prComment` creation to check for the
marker presence, let me know.

Note: it seems like there is currently an issue on mutating comments as
`OUTDATED` through graphql. Although it does not work as expected
(flagging as OUTDATED) the comments are still minimized, which is what
we want.
- https://github.com/orgs/community/discussions/19865

Implements #176

---------

Co-authored-by: Daz DeBoer <daz@gradle.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 10:14:48 -06:00

284 lines
8.7 KiB
TypeScript

import * as core from '@actions/core'
import * as github from '@actions/github'
import {BuildResult} from './build-results'
import {DependencyGraphConfig, getActionId, getGithubToken, getJobMatrix, SummaryConfig} from './configuration'
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
export async function generateJobSummary(
buildResults: BuildResult[],
cachingReport: string,
config: SummaryConfig
): Promise<void> {
const errors = renderErrors()
if (errors) {
core.summary.addRaw(errors)
await core.summary.write()
return
}
const summaryTable = renderSummaryTable(buildResults)
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&nbsp;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
}
}