Skip to content

Commit 3c3fe59

Browse files
cuong-techCuong Pham
and
Cuong Pham
authored
feat: Add last comment validator (#668)
* feat: Add last comment validator * Update changelog Co-authored-by: Cuong Pham <cuong.pham.tech@gmail.com>
1 parent 7b8f60f commit 3c3fe59

File tree

8 files changed

+189
-29
lines changed

8 files changed

+189
-29
lines changed

__tests__/unit/actions/merge.test.js

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,6 @@
11
const Merge = require('../../../lib/actions/merge')
22
const Helper = require('../../../__fixtures__/unit/helper')
33

4-
test('check that merge is called if PR has not been merged', async () => {
5-
const merge = new Merge()
6-
const checkIfMerged = false
7-
const context = Helper.mockContext({ checkIfMerged })
8-
context.octokit.pulls.get.mockReturnValue({ data: { mergeable_state: 'clean', state: 'open' } })
9-
const settings = {}
10-
11-
await merge.afterValidate(context, settings)
12-
expect(context.octokit.pulls.merge.mock.calls.length).toBe(1)
13-
expect(context.octokit.pulls.merge.mock.calls[0][0].merge_method).toBe('merge')
14-
})
15-
16-
test('check that merge is called for review events', async () => {
17-
const merge = new Merge()
18-
const checkIfMerged = false
19-
const context = Helper.mockContext({ checkIfMerged, eventName: 'pull_request_review' })
20-
context.octokit.pulls.get.mockReturnValue({ data: { mergeable_state: 'clean', state: 'open' } })
21-
const settings = {}
22-
23-
await merge.afterValidate(context, settings)
24-
expect(context.octokit.pulls.merge.mock.calls.length).toBe(1)
25-
expect(context.octokit.pulls.merge.mock.calls[0][0].merge_method).toBe('merge')
26-
})
27-
284
test('check that merge is called for status events', async () => {
295
const merge = new Merge()
306
const checkIfMerged = false
@@ -43,10 +19,15 @@ test('check that merge is called for status events', async () => {
4319
expect(context.octokit.pulls.merge.mock.calls[0][0].merge_method).toBe('merge')
4420
})
4521

46-
test('check that merge is called for check suite events', async () => {
22+
test.each([
23+
undefined,
24+
'pull_request_review',
25+
'check_suite',
26+
'issue_comment'
27+
])('check that merge is called for %s events', async (eventName) => {
4728
const merge = new Merge()
4829
const checkIfMerged = false
49-
const context = Helper.mockContext({ checkIfMerged, eventName: 'check_suite' })
30+
const context = Helper.mockContext({ checkIfMerged, eventName })
5031
context.octokit.pulls.get.mockReturnValue({ data: { mergeable_state: 'clean', state: 'open' } })
5132
const settings = {}
5233

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
const LastComment = require('../../../lib/validators/lastComment')
2+
const Helper = require('../../../__fixtures__/unit/helper')
3+
4+
test('validate returns correctly', async () => {
5+
const lastComment = new LastComment()
6+
7+
const settings = {
8+
do: 'lastComment',
9+
must_exclude: {
10+
regex: 'exclude this'
11+
}
12+
}
13+
14+
let results = await lastComment.processValidate(createMockContext(['exclude this']), settings)
15+
expect(results.status).toBe('fail')
16+
17+
results = await lastComment.processValidate(createMockContext(['a', 'b']), settings)
18+
expect(results.status).toBe('pass')
19+
})
20+
21+
test('fail gracefully if invalid regex', async () => {
22+
const lastComment = new LastComment()
23+
24+
const settings = {
25+
do: 'lastComment',
26+
must_exclude: {
27+
regex: '@#$@#$@#$'
28+
}
29+
}
30+
31+
const validation = await lastComment.processValidate(createMockContext('WIP'), settings)
32+
expect(validation.status).toBe('pass')
33+
})
34+
35+
test('description is correct', async () => {
36+
const lastComment = new LastComment()
37+
38+
const settings = {
39+
do: 'lastComment',
40+
must_exclude: {
41+
regex: 'Work in Progress'
42+
}
43+
}
44+
45+
let validation = await lastComment.processValidate(createMockContext('Work in Progress'), settings)
46+
47+
expect(validation.status).toBe('fail')
48+
expect(validation.validations[0].description).toBe('lastComment does not exclude "Work in Progress"')
49+
50+
validation = await lastComment.processValidate(createMockContext('Just lastComment'), settings)
51+
expect(validation.validations[0].description).toBe("lastComment must exclude 'Work in Progress'")
52+
})
53+
54+
test('mergeable is true if must_include is the last comment', async () => {
55+
const lastComment = new LastComment()
56+
57+
const settings = {
58+
do: 'lastComment',
59+
must_include: {
60+
regex: 'xyz'
61+
}
62+
}
63+
64+
let validation = await lastComment.processValidate(createMockContext(['abc', 'experimental', 'xyz']), settings)
65+
expect(validation.status).toBe('pass')
66+
67+
validation = await lastComment.processValidate(createMockContext(['Some lastComment', 'xyz', '456']), settings)
68+
expect(validation.status).toBe('fail')
69+
})
70+
71+
test('mergeable is false if must_exclude is one of the lastComment', async () => {
72+
const lastComment = new LastComment()
73+
74+
const settings = {
75+
do: 'lastComment',
76+
must_exclude: {
77+
regex: 'xyz'
78+
}
79+
}
80+
81+
let validation = await lastComment.processValidate(createMockContext(['abc', 'experimental', 'xyz']), settings)
82+
expect(validation.status).toBe('fail')
83+
84+
validation = await lastComment.processValidate(createMockContext(['Some lastComment', 'xyz', '456']), settings)
85+
expect(validation.status).toBe('pass')
86+
})
87+
88+
test('complex Logic test', async () => {
89+
const lastComment = new LastComment()
90+
91+
const settings = {
92+
do: 'lastComment',
93+
or: [{
94+
and: [{
95+
must_include: {
96+
regex: 'release note: yes',
97+
message: 'Please include release note: yes'
98+
}
99+
}, {
100+
must_include: {
101+
regex: 'lang\\/core|lang\\/c\\+\\+|lang\\/c#',
102+
message: 'Please include a language comment'
103+
}
104+
}]
105+
}, {
106+
must_include: {
107+
regex: 'release note: no',
108+
message: 'Please include release note: no'
109+
}
110+
}]
111+
}
112+
113+
let validation = await lastComment.processValidate(createMockContext(['experimental', 'xyz', 'release note: no']), settings)
114+
expect(validation.status).toBe('pass')
115+
116+
validation = await lastComment.processValidate(createMockContext(['123', '456', 'release note: yes']), settings)
117+
expect(validation.status).toBe('fail')
118+
expect(validation.validations[0].description).toBe('((Please include a language comment) ***OR*** Please include release note: no)')
119+
120+
validation = await lastComment.processValidate(createMockContext(['456', 'lang/core']), settings)
121+
expect(validation.validations[0].description).toBe('((Please include release note: yes) ***OR*** Please include release note: no)')
122+
})
123+
124+
function createMockContext (comments) {
125+
return Helper.mockContext({ listComments: Array.isArray(comments) ? comments.map(comment => ({ body: comment })) : [{ body: comments }] })
126+
}

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
CHANGELOG
22
=====================================
3+
| September 28, 2022: feat: Add last comment validator `#668 <https://github.com/mergeability/mergeable/pull/668>`_
34
| August 26, 2022: set fallback for `no_empty` options processor `#657 <https://github.com/mergeability/mergeable/pull/657>`_
45
| August 25, 2022: fix: set fallback value for `description` payload `#655 <https://github.com/mergeability/mergeable/pull/655>`_
56
| August 20, 2022: fix: supported events for `request_review` action `#651 <https://github.com/mergeability/mergeable/pull/651>`_

docs/validators/last comment.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Last Comment
2+
^^^^^^^^^^
3+
4+
::
5+
6+
- do: lastComment // check if the last comment contains only the word 'merge'
7+
must_include:
8+
regex: '^merge$'
9+
10+
Supported Events:
11+
::
12+
13+
'pull_request.*', 'pull_request_review.*', 'issues.*', 'issue_comment.*'
14+

lib/actions/merge.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ class Merge extends Action {
3131
'pull_request.*',
3232
'pull_request_review.*',
3333
'status.*',
34-
'check_suite.*'
34+
'check_suite.*',
35+
'issue_comment.*'
3536
]
3637

3738
this.supportedSettings = {

lib/eventAware.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class EventAware {
1919
return context.payload
2020
}
2121

22-
if (context.eventName === 'issues') { // event name is 'issues' but payload contain 'issue'
22+
if (context.eventName === 'issues' || context.eventName === 'issue_comment') {
2323
return context.payload.issue
2424
} else if (context.eventName === 'pull_request_review') {
2525
return context.payload.pull_request

lib/filters/payload.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class Payload extends Filter {
2626
this.supportedEvents = [
2727
'pull_request.*',
2828
'pull_request_review.*',
29-
'issues.*'
29+
'issues.*',
30+
'issue_comment.*'
3031
]
3132
// no specific supported settings because it can vary by events
3233
this.supportedSettings = {}

lib/validators/lastComment.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const { Validator } = require('./validator')
2+
3+
class LastComment extends Validator {
4+
constructor () {
5+
super('lastComment')
6+
this.supportedEvents = [
7+
'pull_request.*',
8+
'pull_request_review.*',
9+
'issues.*',
10+
'issue_comment.*'
11+
]
12+
this.supportedSettings = {
13+
must_include: {
14+
regex: ['string', 'array'],
15+
regex_flag: 'string',
16+
message: 'string'
17+
},
18+
must_exclude: {
19+
regex: ['string', 'array'],
20+
regex_flag: 'string',
21+
message: 'string'
22+
}
23+
}
24+
}
25+
26+
async validate (context, validationSettings) {
27+
const comments = (await this.githubAPI.listComments(context, this.getPayload(context).number)).data
28+
29+
return this.processOptions(
30+
validationSettings,
31+
comments.length ? comments[comments.length - 1].body : ''
32+
)
33+
}
34+
}
35+
36+
module.exports = LastComment

0 commit comments

Comments
 (0)