Skip to content

Commit f21eb50

Browse files
committed
Update PR comment behavior for on-failure mode
When comment_summary_in_pr is set to 'on-failure', the action now updates existing comments when issues are resolved in subsequent runs, providing clear feedback that dependency issues have been fixed.
1 parent da24556 commit f21eb50

4 files changed

Lines changed: 269 additions & 9 deletions

File tree

__tests__/comment-pr.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import {expect, test, beforeEach} from '@jest/globals'
2+
3+
// Mock GitHub Actions modules before importing anything else
4+
jest.mock('@actions/github')
5+
jest.mock('@actions/core')
6+
7+
// Create mocks that will be available throughout the module
8+
const createCommentMock = jest.fn()
9+
const updateCommentMock = jest.fn()
10+
const listCommentsMock = jest.fn()
11+
const paginateIteratorMock = jest.fn()
12+
13+
// Mock the octokit instance creation
14+
jest.mock('@actions/github/lib/utils', () => ({
15+
GitHub: {
16+
plugin: jest.fn(() =>
17+
jest.fn(() => ({
18+
rest: {
19+
issues: {
20+
createComment: createCommentMock,
21+
updateComment: updateCommentMock,
22+
listComments: listCommentsMock
23+
}
24+
},
25+
paginate: {
26+
iterator: paginateIteratorMock
27+
}
28+
}))
29+
)
30+
},
31+
getOctokitOptions: jest.fn(() => ({}))
32+
}))
33+
34+
jest.mock('@octokit/plugin-retry', () => ({
35+
retry: {}
36+
}))
37+
38+
// Now import the modules that use the mocks
39+
import {commentPr} from '../src/comment-pr'
40+
import {ConfigurationOptions} from '../src/schemas'
41+
import * as github from '@actions/github'
42+
import * as core from '@actions/core'
43+
44+
const mockGithub = github as jest.Mocked<typeof github>
45+
const mockCore = core as jest.Mocked<typeof core>
46+
47+
const defaultConfig: ConfigurationOptions = {
48+
comment_summary_in_pr: 'on-failure',
49+
fail_on_severity: 'high',
50+
fail_on_scopes: ['runtime'],
51+
allow_licenses: [],
52+
deny_licenses: [],
53+
allow_dependencies_licenses: [],
54+
allow_ghsas: [],
55+
license_check: true,
56+
vulnerability_check: true,
57+
warn_only: false,
58+
show_openssf_scorecard: false,
59+
warn_on_openssf_scorecard_level: 3,
60+
retry_on_snapshot_warnings: false,
61+
retry_on_snapshot_warnings_timeout: 120,
62+
base_ref: '',
63+
head_ref: '',
64+
deny_packages: [],
65+
deny_groups: []
66+
}
67+
68+
beforeEach(() => {
69+
jest.clearAllMocks()
70+
71+
// Setup GitHub context
72+
Object.defineProperty(mockGithub, 'context', {
73+
value: {
74+
repo: {owner: 'test-owner', repo: 'test-repo'},
75+
payload: {pull_request: {number: 123}}
76+
},
77+
writable: true
78+
})
79+
80+
mockCore.getInput.mockReturnValue('mock-token')
81+
82+
// Setup default mock returns
83+
createCommentMock.mockResolvedValue({data: {id: 1}})
84+
updateCommentMock.mockResolvedValue({data: {id: 1}})
85+
listCommentsMock.mockResolvedValue({data: []})
86+
paginateIteratorMock.mockReturnValue([{data: []}])
87+
})
88+
89+
test('commentPr creates comment in always mode regardless of issues', async () => {
90+
const config = {...defaultConfig, comment_summary_in_pr: 'always' as const}
91+
92+
await commentPr('Test content', config, false)
93+
94+
expect(createCommentMock).toHaveBeenCalledWith({
95+
owner: 'test-owner',
96+
repo: 'test-repo',
97+
issue_number: 123,
98+
body: 'Test content\n\n<!-- dependency-review-pr-comment-marker -->'
99+
})
100+
expect(updateCommentMock).not.toHaveBeenCalled()
101+
})
102+
103+
test('commentPr creates comment in on-failure mode when issues found', async () => {
104+
const config = {
105+
...defaultConfig,
106+
comment_summary_in_pr: 'on-failure' as const
107+
}
108+
109+
await commentPr('Issues found', config, true)
110+
111+
expect(createCommentMock).toHaveBeenCalledWith({
112+
owner: 'test-owner',
113+
repo: 'test-repo',
114+
issue_number: 123,
115+
body: 'Issues found\n\n<!-- dependency-review-pr-comment-marker -->'
116+
})
117+
expect(updateCommentMock).not.toHaveBeenCalled()
118+
})
119+
120+
test('commentPr does not comment in never mode', async () => {
121+
const config = {...defaultConfig, comment_summary_in_pr: 'never' as const}
122+
123+
await commentPr('Test content', config, false)
124+
125+
expect(createCommentMock).not.toHaveBeenCalled()
126+
expect(updateCommentMock).not.toHaveBeenCalled()
127+
})
128+
129+
test('commentPr does not comment in on-failure mode when no issues and no existing comment', async () => {
130+
const config = {
131+
...defaultConfig,
132+
comment_summary_in_pr: 'on-failure' as const
133+
}
134+
135+
await commentPr('No issues', config, false)
136+
137+
expect(createCommentMock).not.toHaveBeenCalled()
138+
expect(updateCommentMock).not.toHaveBeenCalled()
139+
})
140+
141+
test('commentPr updates existing comment in on-failure mode when no issues but comment exists', async () => {
142+
const config = {
143+
...defaultConfig,
144+
comment_summary_in_pr: 'on-failure' as const
145+
}
146+
147+
// Mock existing comment
148+
const existingComment = {
149+
id: 456,
150+
body: 'Previous issues\n\n<!-- dependency-review-pr-comment-marker -->'
151+
}
152+
paginateIteratorMock.mockReturnValue([{data: [existingComment]}])
153+
154+
await commentPr('Issues resolved', config, false)
155+
156+
expect(createCommentMock).not.toHaveBeenCalled()
157+
expect(updateCommentMock).toHaveBeenCalledWith({
158+
owner: 'test-owner',
159+
repo: 'test-repo',
160+
comment_id: 456,
161+
body: 'Issues resolved\n\n<!-- dependency-review-pr-comment-marker -->'
162+
})
163+
})
164+
165+
test('commentPr updates existing comment instead of creating new one in always mode', async () => {
166+
const config = {...defaultConfig, comment_summary_in_pr: 'always' as const}
167+
168+
// Mock existing comment
169+
const existingComment = {
170+
id: 789,
171+
body: 'Old content\n\n<!-- dependency-review-pr-comment-marker -->'
172+
}
173+
paginateIteratorMock.mockReturnValue([{data: [existingComment]}])
174+
175+
await commentPr('Updated content', config, false)
176+
177+
expect(createCommentMock).not.toHaveBeenCalled()
178+
expect(updateCommentMock).toHaveBeenCalledWith({
179+
owner: 'test-owner',
180+
repo: 'test-repo',
181+
comment_id: 789,
182+
body: 'Updated content\n\n<!-- dependency-review-pr-comment-marker -->'
183+
})
184+
})
185+
186+
test('commentPr finds comment marker among multiple comments', async () => {
187+
const config = {...defaultConfig, comment_summary_in_pr: 'always' as const}
188+
189+
// Mock multiple comments with marker in the middle
190+
const comments = [
191+
{id: 1, body: 'Regular comment'},
192+
{id: 2, body: 'Another comment'},
193+
{
194+
id: 3,
195+
body: 'Target comment\n\n<!-- dependency-review-pr-comment-marker -->'
196+
},
197+
{id: 4, body: 'Last comment'}
198+
]
199+
paginateIteratorMock.mockReturnValue([{data: comments}])
200+
201+
await commentPr('Updated content', config, false)
202+
203+
expect(createCommentMock).not.toHaveBeenCalled()
204+
expect(updateCommentMock).toHaveBeenCalledWith({
205+
owner: 'test-owner',
206+
repo: 'test-repo',
207+
comment_id: 3,
208+
body: 'Updated content\n\n<!-- dependency-review-pr-comment-marker -->'
209+
})
210+
})
211+
212+
test('commentPr handles non-PR context gracefully', async () => {
213+
Object.defineProperty(mockGithub, 'context', {
214+
value: {
215+
repo: {owner: 'test-owner', repo: 'test-repo'},
216+
payload: {}
217+
},
218+
writable: true
219+
})
220+
221+
const config = {...defaultConfig, comment_summary_in_pr: 'always' as const}
222+
223+
await commentPr('Test content', config, false)
224+
225+
expect(mockCore.warning).toHaveBeenCalledWith(
226+
'Not in the context of a pull request. Skipping comment creation.'
227+
)
228+
expect(createCommentMock).not.toHaveBeenCalled()
229+
expect(updateCommentMock).not.toHaveBeenCalled()
230+
})

dist/index.js

Lines changed: 18 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/comment-pr.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ export async function commentPr(
2020
config: ConfigurationOptions,
2121
issueFound: boolean
2222
): Promise<void> {
23-
if (
24-
!(
25-
config.comment_summary_in_pr === 'always' ||
26-
(config.comment_summary_in_pr === 'on-failure' && issueFound)
27-
)
28-
) {
23+
// For 'on-failure' mode, we need to handle both failure and success cases
24+
// - On failure: create/update comment with issues
25+
// - On success: update existing comment to show resolution (if one exists)
26+
const shouldComment =
27+
config.comment_summary_in_pr === 'always' ||
28+
(config.comment_summary_in_pr === 'on-failure' && issueFound) ||
29+
(config.comment_summary_in_pr === 'on-failure' &&
30+
!issueFound &&
31+
(await hasExistingComment()))
32+
33+
if (!shouldComment) {
2934
return
3035
}
3136

@@ -75,6 +80,15 @@ export async function commentPr(
7580
}
7681
}
7782

83+
async function hasExistingComment(): Promise<boolean> {
84+
if (!github.context.payload.pull_request) {
85+
return false
86+
}
87+
88+
const existingCommentId = await findCommentByMarker(COMMENT_MARKER)
89+
return existingCommentId !== undefined
90+
}
91+
7892
async function findCommentByMarker(
7993
commentBodyIncludes: string
8094
): Promise<number | undefined> {

0 commit comments

Comments
 (0)