diff --git a/docs/setup-gradle.md b/docs/setup-gradle.md index cd9dcfa1..79d8e969 100644 --- a/docs/setup-gradle.md +++ b/docs/setup-gradle.md @@ -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. diff --git a/sources/src/configuration.ts b/sources/src/configuration.ts index 3e519e49..d698efbf 100644 --- a/sources/src/configuration.ts +++ b/sources/src/configuration.ts @@ -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) } diff --git a/sources/src/job-summary.ts b/sources/src/job-summary.ts index 08d53ca7..3408fe21 100644 --- a/sources/src/job-summary.ts +++ b/sources/src/job-summary.ts @@ -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 { const pull_request_number = context.payload.pull_request.number core.info(`Adding Job Summary as comment to PR #${pull_request_number}.`) - const prComment = `

Job Summary for Gradle

+ const prComment = `${jobMarker(context)} +

Job Summary for Gradle

${context.workflow} :: ${context.job}
@@ -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 { + 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(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 `` +} + +interface PullRequestComment { + id: string + body: string + isMinimized: boolean + url: string +} + +interface CommentsQueryResult { + repository: { + pullRequest?: { + comments?: { + nodes?: (PullRequestComment | null)[] | null + } | null + } | null + } +} diff --git a/sources/test/jest/job-summary.test.ts b/sources/test/jest/job-summary.test.ts index 10197add..1ec3c5ce 100644 --- a/sources/test/jest/job-summary.test.ts +++ b/sources/test/jest/job-summary.test.ts @@ -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('') + }) + + 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('') + }) +})