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>
This commit is contained in:
Simon Marquis
2026-06-10 10:14:48 -06:00
committed by GitHub
co-authored by Daz DeBoer Claude Opus 4.8
parent a740661292
commit 318eed7038
4 changed files with 124 additions and 4 deletions
+5
View File
@@ -548,6 +548,11 @@ jobs:
- run: ./gradlew build --scan
```
When a comment is added, any earlier Job Summary comments posted by the same job on that Pull Request are
automatically minimized, so only the most recent result remains expanded. This also applies when
`add-job-summary-as-pr-comment: 'on-failure'` is used: once a later run of the job succeeds, the previous
failure comment is collapsed.
Note that to add a Pull Request comment, the workflow must be configured with the `pull-requests: write` permission.
+4
View File
@@ -206,6 +206,10 @@ export class SummaryConfig {
return this.shouldAddJobSummary(this.getJobSummaryOption(), hasFailure)
}
canAddPRComment(): boolean {
return this.getPRCommentOption() !== JobSummaryOption.Never
}
shouldAddPRComment(hasFailure: boolean): boolean {
return this.shouldAddJobSummary(this.getPRCommentOption(), hasFailure)
}
+82 -2
View File
@@ -2,7 +2,7 @@ import * as core from '@actions/core'
import * as github from '@actions/github'
import {BuildResult} from './build-results'
import {SummaryConfig, getActionId, getGithubToken} from './configuration'
import {DependencyGraphConfig, getActionId, getGithubToken, getJobMatrix, SummaryConfig} from './configuration'
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
export async function generateJobSummary(
@@ -33,6 +33,10 @@ export async function generateJobSummary(
core.info('============================')
}
if (config.canAddPRComment()) {
await minimizeObsoletePRComments()
}
if (config.shouldAddPRComment(hasFailure)) {
await addPRComment(summaryTable)
}
@@ -48,7 +52,8 @@ async function addPRComment(jobSummary: string): Promise<void> {
const pull_request_number = context.payload.pull_request.number
core.info(`Adding Job Summary as comment to PR #${pull_request_number}.`)
const prComment = `<h3>Job Summary for Gradle</h3>
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>
@@ -57,6 +62,7 @@ ${jobSummary}`
const github_token = getGithubToken()
const octokit = github.getOctokit(github_token)
try {
await octokit.rest.issues.createComment({
...context.repo,
@@ -201,3 +207,77 @@ function truncateString(str: string, maxLength: number): string {
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
}
}
+33 -2
View File
@@ -1,8 +1,15 @@
import dedent from 'dedent'
import {describe, expect, it} from '@jest/globals'
import * as github from '@actions/github'
import {afterEach, describe, expect, it} from '@jest/globals'
import {BuildResult} from '../../src/build-results'
import {renderSummaryTable} from '../../src/job-summary'
import {jobMarker, renderSummaryTable} from '../../src/job-summary'
const MATRIX_INPUT_ENV = 'INPUT_WORKFLOW-JOB-CONTEXT'
function fakeContext(workflow: string, job: string): typeof github.context {
return {workflow, job} as unknown as typeof github.context
}
const successfulHelpBuild: BuildResult = {
rootProjectName: 'root',
@@ -177,3 +184,27 @@ describe('renderSummaryTable', () => {
})
})
})
describe('jobMarker', () => {
const original = process.env[MATRIX_INPUT_ENV]
afterEach(() => {
if (original === undefined) {
delete process.env[MATRIX_INPUT_ENV]
} else {
process.env[MATRIX_INPUT_ENV] = original
}
})
it('builds a hidden marker from the workflow and job', () => {
process.env[MATRIX_INPUT_ENV] = 'null'
const marker = jobMarker(fakeContext('CI', 'build'))
expect(marker).toBe('<!-- gradle-job-summary: ci-build -->')
})
it('includes the job matrix in the marker', () => {
process.env[MATRIX_INPUT_ENV] = JSON.stringify({os: 'ubuntu', java: '17'})
const marker = jobMarker(fakeContext('CI', 'build'))
expect(marker).toBe('<!-- gradle-job-summary: ci-build-ubuntu-17 -->')
})
})