Skip to content

Commit b0681fe

Browse files
authored
fix: Catch errors when adding a github comment and write to the step summary instead (#15)
Adding a GitHub comment might fail due to invalid rights configured in the workflow / a bot user / ... We should not fail the step but log an error and fall back to writing to the step summary.
1 parent 0367041 commit b0681fe

3 files changed

Lines changed: 152 additions & 7 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
This is a github action which is supposed to be run for a PR and visualizes the test coverage.
44

55
Everytime this action is run, we add a comment to the PR showing the current coverage.
6+
If GitHub does not allow creating or updating a PR comment, the action falls back to the step summary instead.
67

78
If the trigger is not `pull_request`, this action does nothing. Also, if the length of the comment would exceed the GitHub limit, lines are trunctuated.
89

@@ -20,6 +21,7 @@ on: pull_request
2021

2122
permissions:
2223
contents: read
24+
issues: write
2325
pull-requests: write
2426

2527
jobs:

src/action.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ const actionLogger: Logger = {
2020
debug: (message) => core.debug(message),
2121
}
2222

23+
const formatCommentError = (error: unknown): string => {
24+
const message = error instanceof Error ? error.message : String(error)
25+
26+
if (typeof error === 'object' && error !== null && 'status' in error) {
27+
const status = error.status
28+
if (typeof status === 'number' || typeof status === 'string') {
29+
return `status ${status}: ${message}`
30+
}
31+
}
32+
33+
return message
34+
}
35+
2336
export const run = async (inputs: Inputs, octokit: Octokit, context: Context): Promise<void> => {
2437
const shas = getComparisonShas(context)
2538

@@ -67,13 +80,22 @@ export const run = async (inputs: Inputs, octokit: Octokit, context: Context): P
6780
}
6881

6982
// Post or update comment
70-
const { url, updated } = await postComment(octokit, context, pullNumber, markdown, inputs.updateComment)
83+
try {
84+
const { url, updated } = await postComment(octokit, context, pullNumber, markdown, inputs.updateComment)
7185

72-
if (updated) {
73-
core.info(`Updated existing comment: ${url}`)
74-
} else {
75-
core.info(`Created new comment: ${url}`)
76-
}
86+
if (updated) {
87+
core.info(`Updated existing comment: ${url}`)
88+
} else {
89+
core.info(`Created new comment: ${url}`)
90+
}
7791

78-
core.info('Coverage visualization posted successfully')
92+
core.info('Coverage visualization posted successfully')
93+
} catch (error) {
94+
core.warning(
95+
`Failed to post or update PR comment (${formatCommentError(error)}). Falling back to the step summary instead.`,
96+
)
97+
await core.summary.addRaw(markdown).write()
98+
core.info('Coverage visualization written to step summary')
99+
return
100+
}
79101
}

tests/action.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { Octokit } from '@octokit/action'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { type Inputs, run } from '../src/action.js'
4+
import type { Context } from '../src/github.js'
5+
6+
const mocks = vi.hoisted(() => ({
7+
processCoverage: vi.fn(),
8+
findPullRequestNumber: vi.fn(),
9+
getComparisonShas: vi.fn(),
10+
postComment: vi.fn(),
11+
info: vi.fn(),
12+
warning: vi.fn(),
13+
debug: vi.fn(),
14+
setOutput: vi.fn(),
15+
summaryAddRaw: vi.fn(),
16+
summaryWrite: vi.fn(),
17+
}))
18+
19+
vi.mock('@actions/core', () => ({
20+
info: mocks.info,
21+
warning: mocks.warning,
22+
debug: mocks.debug,
23+
setOutput: mocks.setOutput,
24+
summary: {
25+
addRaw: mocks.summaryAddRaw,
26+
},
27+
}))
28+
29+
vi.mock('../src/core/index.js', () => ({
30+
processCoverage: mocks.processCoverage,
31+
}))
32+
33+
vi.mock('../src/github.js', () => ({
34+
findPullRequestNumber: mocks.findPullRequestNumber,
35+
getComparisonShas: mocks.getComparisonShas,
36+
postComment: mocks.postComment,
37+
}))
38+
39+
const inputs: Inputs = {
40+
files: 'coverage.xml',
41+
updateComment: true,
42+
showChangedLinesOnly: true,
43+
excludeFilesPattern: '',
44+
sourceDir: '/repo',
45+
}
46+
47+
const context = {
48+
repo: {
49+
owner: 'owner',
50+
repo: 'repo',
51+
},
52+
sha: 'head-sha',
53+
payload: {
54+
pull_request: {
55+
number: 123,
56+
state: 'open',
57+
},
58+
},
59+
} as unknown as Context
60+
61+
const octokit = {} as Octokit
62+
63+
describe('run', () => {
64+
beforeEach(() => {
65+
vi.clearAllMocks()
66+
mocks.summaryWrite.mockResolvedValue(undefined)
67+
mocks.summaryAddRaw.mockReturnValue({ write: mocks.summaryWrite })
68+
mocks.getComparisonShas.mockReturnValue({ baseSha: 'base-sha', headSha: 'head-sha' })
69+
mocks.processCoverage.mockResolvedValue({
70+
markdown: 'coverage markdown',
71+
lineCoverage: 0.75,
72+
branchCoverage: 0.5,
73+
lineCoveragePr: 0.8,
74+
branchCoveragePr: undefined,
75+
})
76+
})
77+
78+
it('posts a PR comment when comment posting succeeds', async () => {
79+
mocks.findPullRequestNumber.mockResolvedValue(123)
80+
mocks.postComment.mockResolvedValue({
81+
url: 'https://github.com/owner/repo/pull/123#issuecomment-1',
82+
updated: false,
83+
})
84+
85+
await run(inputs, octokit, context)
86+
87+
expect(mocks.postComment).toHaveBeenCalledWith(octokit, context, 123, 'coverage markdown', true)
88+
expect(mocks.summaryAddRaw).not.toHaveBeenCalled()
89+
expect(mocks.warning).not.toHaveBeenCalled()
90+
expect(mocks.info).toHaveBeenCalledWith(
91+
'Created new comment: https://github.com/owner/repo/pull/123#issuecomment-1',
92+
)
93+
expect(mocks.info).toHaveBeenCalledWith('Coverage visualization posted successfully')
94+
})
95+
96+
it('falls back to the step summary when comment posting fails', async () => {
97+
const error = Object.assign(new Error('Resource not accessible by integration'), { status: 403 })
98+
mocks.findPullRequestNumber.mockResolvedValue(123)
99+
mocks.postComment.mockRejectedValue(error)
100+
101+
await expect(run(inputs, octokit, context)).resolves.toBeUndefined()
102+
103+
expect(mocks.warning).toHaveBeenCalledWith(
104+
'Failed to post or update PR comment (status 403: Resource not accessible by integration). Falling back to the step summary instead.',
105+
)
106+
expect(mocks.summaryAddRaw).toHaveBeenCalledWith('coverage markdown')
107+
expect(mocks.summaryWrite).toHaveBeenCalledTimes(1)
108+
expect(mocks.info).toHaveBeenCalledWith('Coverage visualization written to step summary')
109+
})
110+
111+
it('writes to the step summary when no PR is found', async () => {
112+
mocks.findPullRequestNumber.mockResolvedValue(null)
113+
114+
await run(inputs, octokit, context)
115+
116+
expect(mocks.postComment).not.toHaveBeenCalled()
117+
expect(mocks.summaryAddRaw).toHaveBeenCalledWith('coverage markdown')
118+
expect(mocks.summaryWrite).toHaveBeenCalledTimes(1)
119+
expect(mocks.info).toHaveBeenCalledWith('No pull request found for this commit, writing to step summary instead')
120+
})
121+
})

0 commit comments

Comments
 (0)