Skip to content

Commit d307350

Browse files
authored
Merge pull request #1379 from mikepenz/copilot/fix-1378
Add pr_id parameter to support PR comments from workflow_run contexts
2 parents a83fd2b + c887f44 commit d307350

File tree

7 files changed

+252
-17
lines changed

7 files changed

+252
-17
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ jobs:
114114
| `truncate_stack_traces` | Optional. Truncate stack traces from test output to 2 lines in annotations. Defaults to `true`. |
115115
| `resolve_ignore_classname` | Optional. Force ignore test case classname from the xml report (This can help fix issues with some tools/languages). Defaults to `false`. |
116116
| `skip_comment_without_tests` | Optional. Disable commenting if no tests are detected. Defaults to `false`. |
117+
| `pr_id` | Optional. PR number to comment on (useful for workflow_run contexts where the action runs outside the PR context). When provided, overrides the automatic PR detection. |
117118

118119
### Common Configurations
119120

@@ -247,10 +248,16 @@ jobs:
247248
with:
248249
commit: ${{github.event.workflow_run.head_sha}}
249250
report_paths: '**/build/test-results/test/TEST-*.xml'
251+
# Optional: if you want to add PR comments from workflow_run context
252+
# comment: true
253+
# pr_id: ${{ github.event.workflow_run.pull_requests[0].number }}
250254
```
251255

252256
This will securely post the check results from the privileged workflow onto the PR's checks report.
253257

258+
> [!TIP]
259+
> When running from `workflow_run` context, use the `pr_id` parameter to enable PR comments: `pr_id: ${{ github.event.workflow_run.pull_requests[0].number }}`
260+
254261
</p>
255262
</details>
256263

__tests__/annotator.test.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import {jest} from '@jest/globals'
2+
import {attachComment, buildCommentIdentifier} from '../src/annotator.js'
3+
import * as core from '@actions/core'
4+
5+
/**
6+
* Copyright 2024 Mike Penz
7+
*/
8+
jest.setTimeout(30000)
9+
10+
// Mock the context object
11+
jest.mock('@actions/github/lib/utils.js', () => ({
12+
context: {
13+
issue: {number: undefined},
14+
repo: {owner: 'test-owner', repo: 'test-repo'}
15+
}
16+
}))
17+
18+
describe('attachComment', () => {
19+
let mockOctokit: any
20+
let mockWarning: jest.SpiedFunction<typeof core.warning>
21+
let mockContext: any
22+
23+
beforeEach(() => {
24+
// Import context after mocking
25+
const {context} = require('@actions/github/lib/utils.js')
26+
mockContext = context
27+
28+
// Mock core.warning
29+
mockWarning = jest.spyOn(core, 'warning').mockImplementation(() => {})
30+
31+
// Mock octokit
32+
mockOctokit = {
33+
paginate: jest.fn(),
34+
rest: {
35+
issues: {
36+
listComments: jest.fn(),
37+
createComment: jest.fn(),
38+
updateComment: jest.fn()
39+
}
40+
}
41+
}
42+
})
43+
44+
afterEach(() => {
45+
jest.restoreAllMocks()
46+
})
47+
48+
it('should use pr_id when provided and context.issue.number is not available', async () => {
49+
// Setup: no context issue number
50+
mockContext.issue.number = undefined
51+
52+
mockOctokit.paginate.mockResolvedValue([])
53+
54+
const checkName = ['Test Check']
55+
const table = [['Test', 'Result'], ['Example Test', 'Passed']]
56+
const prId = '123'
57+
58+
await attachComment(mockOctokit, checkName, false, table, [], [], [], prId)
59+
60+
// Verify comment was created with correct issue number
61+
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledWith({
62+
owner: 'test-owner',
63+
repo: 'test-repo',
64+
issue_number: 123,
65+
body: expect.stringContaining('Example Test')
66+
})
67+
68+
expect(mockWarning).not.toHaveBeenCalled()
69+
})
70+
71+
it('should fall back to context.issue.number when pr_id is not provided', async () => {
72+
// Setup: context issue number available
73+
mockContext.issue.number = 456
74+
75+
mockOctokit.paginate.mockResolvedValue([])
76+
77+
const checkName = ['Test Check']
78+
const table = [['Test', 'Result'], ['Example Test', 'Passed']]
79+
80+
await attachComment(mockOctokit, checkName, false, table, [], [], [])
81+
82+
// Verify comment was created with context issue number
83+
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledWith({
84+
owner: 'test-owner',
85+
repo: 'test-repo',
86+
issue_number: 456,
87+
body: expect.stringContaining('Example Test')
88+
})
89+
90+
expect(mockWarning).not.toHaveBeenCalled()
91+
})
92+
93+
it('should warn and return early when no issue number is available', async () => {
94+
// Setup: no context issue number and no pr_id
95+
mockContext.issue.number = undefined
96+
97+
const checkName = ['Test Check']
98+
const table = [['Test', 'Result'], ['Example Test', 'Passed']]
99+
100+
await attachComment(mockOctokit, checkName, false, table, [], [], [])
101+
102+
// Verify warning was called and no comment was created
103+
expect(mockWarning).toHaveBeenCalledWith(
104+
expect.stringContaining('Action requires a valid issue number (PR reference) or pr_id input')
105+
)
106+
expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled()
107+
})
108+
109+
it('should update existing comment when updateComment is true', async () => {
110+
// Setup: context issue number available
111+
mockContext.issue.number = 456
112+
113+
const existingComment = {
114+
id: 999,
115+
body: 'Existing comment <!-- Summary comment for ["Test Check"] by mikepenz/action-junit-report -->'
116+
}
117+
mockOctokit.paginate.mockResolvedValue([existingComment])
118+
119+
const checkName = ['Test Check']
120+
const table = [['Test', 'Result'], ['Example Test', 'Updated']]
121+
122+
await attachComment(mockOctokit, checkName, true, table, [], [], [])
123+
124+
// Verify comment was updated
125+
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
126+
owner: 'test-owner',
127+
repo: 'test-repo',
128+
comment_id: 999,
129+
body: expect.stringContaining('Example Test')
130+
})
131+
expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled()
132+
})
133+
it('should warn and return early when pr_id is invalid', async () => {
134+
// Setup: no context issue number and invalid pr_id
135+
mockContext.issue.number = undefined
136+
137+
const checkName = ['Test Check']
138+
const table = [['Test', 'Result'], ['Example Test', 'Passed']]
139+
const prId = 'invalid-number'
140+
141+
await attachComment(mockOctokit, checkName, false, table, [], [], [], prId)
142+
143+
// Verify warning was called and no comment was created
144+
expect(mockWarning).toHaveBeenCalledWith(
145+
expect.stringContaining('Action requires a valid issue number (PR reference) or pr_id input')
146+
)
147+
expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled()
148+
})
149+
150+
it('should handle pr_id with leading/trailing whitespace', async () => {
151+
// Setup: no context issue number
152+
mockContext.issue.number = undefined
153+
154+
mockOctokit.paginate.mockResolvedValue([])
155+
156+
const checkName = ['Test Check']
157+
const table = [['Test', 'Result'], ['Example Test', 'Passed']]
158+
const prId = ' 123 '
159+
160+
await attachComment(mockOctokit, checkName, false, table, [], [], [], prId)
161+
162+
// Verify comment was created with correct issue number (whitespace trimmed)
163+
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledWith({
164+
owner: 'test-owner',
165+
repo: 'test-repo',
166+
issue_number: 123,
167+
body: expect.stringContaining('Example Test')
168+
})
169+
170+
expect(mockWarning).not.toHaveBeenCalled()
171+
})
172+
173+
it('should update existing comment when pr_id is provided and updateComment is true', async () => {
174+
// Setup: no context issue number but pr_id provided
175+
mockContext.issue.number = undefined
176+
177+
const existingComment = {
178+
id: 888,
179+
body: 'Existing comment <!-- Summary comment for ["Test Check"] by mikepenz/action-junit-report -->'
180+
}
181+
mockOctokit.paginate.mockResolvedValue([existingComment])
182+
183+
const checkName = ['Test Check']
184+
const table = [['Test', 'Result'], ['Example Test', 'Updated']]
185+
const prId = '789'
186+
187+
await attachComment(mockOctokit, checkName, true, table, [], [], [], prId)
188+
189+
// Verify paginate was called with correct issue number
190+
expect(mockOctokit.paginate).toHaveBeenCalledWith(
191+
mockOctokit.rest.issues.listComments,
192+
{
193+
owner: 'test-owner',
194+
repo: 'test-repo',
195+
issue_number: 789
196+
}
197+
)
198+
199+
// Verify comment was updated
200+
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
201+
owner: 'test-owner',
202+
repo: 'test-repo',
203+
comment_id: 888,
204+
body: expect.stringContaining('Example Test')
205+
})
206+
expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled()
207+
})
208+
209+
})
210+
211+
describe('buildCommentIdentifier', () => {
212+
it('should build correct identifier', () => {
213+
const checkName = ['Test Check']
214+
const identifier = buildCommentIdentifier(checkName)
215+
expect(identifier).toBe('<!-- Summary comment for ["Test Check"] by mikepenz/action-junit-report -->')
216+
})
217+
})

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ inputs:
160160
description: 'Disable commenting if no tests are detected'
161161
required: false
162162
default: 'false'
163+
pr_id:
164+
description: 'PR number to comment on (useful for workflow_run contexts)'
165+
required: false
163166
outputs:
164167
total:
165168
description: 'The total count of all checks'

dist/index.js

Lines changed: 11 additions & 8 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/annotator.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,14 @@ export async function attachComment(
180180
table: SummaryTableRow[],
181181
detailsTable: SummaryTableRow[],
182182
flakySummary: SummaryTableRow[],
183-
checkInfos: CheckInfo[] = []
183+
checkInfos: CheckInfo[] = [],
184+
prId?: string
184185
): Promise<void> {
185-
if (!context.issue.number) {
186-
core.warning(`⚠️ Action requires a valid issue number (PR reference) to be able to attach a comment..`)
186+
// Use provided prId or fall back to context issue number
187+
const issueNumber = prId ? parseInt(prId, 10) : context.issue.number
188+
189+
if (!issueNumber) {
190+
core.warning(`⚠️ Action requires a valid issue number (PR reference) or pr_id input to be able to attach a comment..`)
187191
return
188192
}
189193

@@ -215,7 +219,7 @@ export async function attachComment(
215219

216220
comment += `\n\n${identifier}`
217221

218-
const priorComment = updateComment ? await findPriorComment(octokit, identifier) : undefined
222+
const priorComment = updateComment ? await findPriorComment(octokit, identifier, issueNumber) : undefined
219223
if (priorComment) {
220224
await octokit.rest.issues.updateComment({
221225
owner: context.repo.owner,
@@ -227,17 +231,17 @@ export async function attachComment(
227231
await octokit.rest.issues.createComment({
228232
owner: context.repo.owner,
229233
repo: context.repo.repo,
230-
issue_number: context.issue.number,
234+
issue_number: issueNumber,
231235
body: comment
232236
})
233237
}
234238
}
235239

236-
async function findPriorComment(octokit: InstanceType<typeof GitHub>, identifier: string): Promise<number | undefined> {
240+
async function findPriorComment(octokit: InstanceType<typeof GitHub>, identifier: string, issueNumber: number): Promise<number | undefined> {
237241
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
238242
owner: context.repo.owner,
239243
repo: context.repo.repo,
240-
issue_number: context.issue.number
244+
issue_number: issueNumber
241245
})
242246

243247
const foundComment = comments.find(comment => comment.body?.endsWith(identifier))

src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export async function run(): Promise<void> {
4141
const updateComment = core.getInput('updateComment') === 'true'
4242
const jobName = core.getInput('job_name')
4343
const skipCommentWithoutTests = core.getInput('skip_comment_without_tests') === 'true'
44+
const prId = core.getInput('pr_id').trim() || undefined
4445

4546
const reportPaths = core.getMultilineInput('report_paths')
4647
const summary = core.getMultilineInput('summary')
@@ -208,7 +209,7 @@ export async function run(): Promise<void> {
208209

209210
if (comment && (!skipCommentWithoutTests || mergedResult.totalCount > 0)) {
210211
const octokit: InstanceType<typeof GitHub> = github.getOctokit(token)
211-
await attachComment(octokit, checkName, updateComment, table, detailTable, flakyTable, checkInfos)
212+
await attachComment(octokit, checkName, updateComment, table, detailTable, flakyTable, checkInfos, prId)
212213
}
213214

214215
core.setOutput('summary', buildTable(table))

0 commit comments

Comments
 (0)