Skip to content

Commit

Permalink
Add support for status events to baseRef validator
Browse files Browse the repository at this point in the history
- For status events the commit sha in the payload is used to make an
  octokit request to fetch the pull requests to which the commit
  belongs.
  The pull requests are filtered to just the open ones.
  The validator than check the base refs of these open pull requests.
- Add unit tests to cover new functionality
  • Loading branch information
Abid H. Mujtaba committed Dec 12, 2021
1 parent a430400 commit 45a15e6
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 5 deletions.
178 changes: 178 additions & 0 deletions __tests__/unit/validators/baseRef.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,63 @@
const BaseRef = require('../../../lib/validators/baseRef')
const Helper = require('../../../__fixtures__/unit/helper')

test('validateCheckSuite called for check_suite events', async () => {
// GIVEN
const baseRef = new BaseRef()

const settings = {
do: 'baseRef'
}

const returnValue = {}
baseRef.validateCheckSuite = jest.fn()
baseRef.validateCheckSuite.mockReturnValueOnce(returnValue)

// WHEN
const output = await baseRef.processValidate(mockCheckSuiteContext(['foo']), settings)

// THEN
expect(output).toBe(returnValue)
})

test('validateStatus called for status events', async () => {
// GIVEN
const baseRef = new BaseRef()

const settings = {
do: 'baseRef'
}

const returnValue = {}
baseRef.validateStatus = jest.fn()
baseRef.validateStatus.mockReturnValueOnce(returnValue)

// WHEN
const output = await baseRef.processValidate(mockStatusContext(['foo']), settings)

// THEN
expect(output).toBe(returnValue)
})

test('processOptions called for non-check-suite non-status events', async () => {
// GIVEN
const baseRef = new BaseRef()

const settings = {
do: 'baseRef'
}

const returnValue = {}
baseRef.processOptions = jest.fn()
baseRef.processOptions.mockReturnValueOnce(returnValue)

// WHEN
const output = await baseRef.processValidate(mockContext('foo'), settings)

// THEN
expect(output).toBe(returnValue)
})

test('fail gracefully if invalid regex', async () => {
const baseRef = new BaseRef()

Expand Down Expand Up @@ -158,8 +215,129 @@ const mockContext = baseRef => {
return context
}

test('fail when exclude regex is in baseRef of single pull request related to status', async () => {
const baseRef = new BaseRef()

const settings = {
do: 'baseRef',
must_exclude: {
regex: 'wip'
}
}

const context = mockStatusContext(['WIP foo'])

const baseRefValidation = await baseRef.processValidate(context, settings)
expect(baseRefValidation.status).toBe('fail')
})

test('fail when exclude regex is in one baseRef of multiple pull requests related to status', async () => {
const baseRef = new BaseRef()

const settings = {
do: 'baseRef',
must_exclude: {
regex: 'wip'
}
}

const context = mockStatusContext(['foo', 'WIP bar', 'baz'])

const baseRefValidation = await baseRef.processValidate(context, settings)
expect(baseRefValidation.status).toBe('fail')
})

test('pass when exclude regex is not in any baseRef of multiple pull requests related to status', async () => {
const baseRef = new BaseRef()

const settings = {
do: 'baseRef',
must_exclude: {
regex: 'wip'
}
}

const context = mockStatusContext(['foo', 'bar', 'baz'])

const baseRefValidation = await baseRef.processValidate(context, settings)
expect(baseRefValidation.status).toBe('pass')
})

test('fail when include regex exists and there are no pull requests related to status', async () => {
const baseRef = new BaseRef()

const settings = {
do: 'baseRef',
must_include: {
regex: 'foo'
}
}

const context = mockStatusContext([])

const baseRefValidation = await baseRef.processValidate(context, settings)
expect(baseRefValidation.status).toBe('fail')
})

test('pass when exclude regex is only in baseRef of a closed pull request related to status', async () => {
const baseRef = new BaseRef()

const settings = {
do: 'baseRef',
must_exclude: {
regex: 'wip'
}
}

const context = Helper.mockContext({ eventName: 'status' })
const pulls = {
data: [
{ state: 'open', base: { ref: 'foo' } },
{ state: 'closed', base: { ref: 'wip bar' } }
]
}
context.octokit.request = jest.fn()
context.octokit.request.mockReturnValueOnce(pulls)

const baseRefValidation = await baseRef.processValidate(context, settings)
expect(baseRefValidation.status).toBe('pass')
})

test('fail with mediaType when exclude regex is in baseRef of single pull request related to status', async () => {
const baseRef = new BaseRef()

const settings = {
do: 'baseRef',
must_exclude: {
regex: 'wip'
},
mediaType: {
previews: ['groot']
}
}

const context = mockStatusContext(['WIP foo'])

const baseRefValidation = await baseRef.processValidate(context, settings)
expect(baseRefValidation.status).toBe('fail')

expect(context.octokit.request.mock.calls[0][1].mediaType.previews[0]).toBe('groot')
})

const mockCheckSuiteContext = baseRefs => {
const context = Helper.mockContext({ eventName: 'check_suite' })
context.payload.check_suite.pull_requests = baseRefs.map(baseRef => ({ base: { ref: baseRef } }))
return context
}

const mockStatusContext = baseRefs => {
const context = Helper.mockContext({ eventName: 'status' })
const pulls = {
data: baseRefs.map(baseRef => ({ state: 'open', base: { ref: baseRef } }))
}

context.octokit.request = jest.fn()
context.octokit.request.mockReturnValueOnce(pulls)

return context
}
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
CHANGELOG
=====================================
| December 12, 2021: feat: Add support for status events to baseRef validator `#395 <https://github.com/mergeability/mergeable/issues/395#issuecomment-991904249>` _
| November 25, 2021: feat: Add more supported events to baseRef validator `#395 <https://github.com/mergeability/mergeable/issues/395#issuecomment-975763927>` _
| November 12, 2021 : feat: Add baseRef filter `#596 <https://github.com/mergeability/mergeable/pull/596>`_
| October 19, 2021 : feat: Add validator approval option to exclude users `#594 <https://github.com/mergeability/mergeable/pull/594>`_
Expand Down
16 changes: 15 additions & 1 deletion docs/validators/baseRef.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ BaseRef
regex: 'feature-branch2'
regex_flag: 'none' # Optional. Specify the flag for Regex. default is 'i', to disable default use 'none'
message: 'Custom message...'
mediaType: # Optional. Required by status.* events to enable the groot preview on some Github Enterprise servers
previews: 'array'


Simple example:
Expand All @@ -23,7 +25,19 @@ Simple example:
message: 'Merging into repo:master is forbidden'


Example with groot preview enabled (for status.* events on some older Github Enterprise servers)
::

- do: baseRef
must_include:
regex: 'master|main'
message: 'Auto-merging is only enabled for default branch'
mediaType:
previews:
- groot


Supported Events:
::

'pull_request.*', 'pull_request_review.*', 'check_suite.*'
'pull_request.*', 'pull_request_review.*', 'check_suite.*', status.*
46 changes: 42 additions & 4 deletions lib/validators/baseRef.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class BaseRef extends Validator {
this.supportedEvents = [
'pull_request.*',
'pull_request_review.*',
'check_suite.*'
'check_suite.*',
'status.*'
]
this.supportedSettings = {
must_include: {
Expand All @@ -20,30 +21,67 @@ class BaseRef extends Validator {
regex: ['string', 'array'],
regex_flag: 'string',
message: 'string'
},
mediaType: {
previews: 'array'
}
}
}

async validate (context, validationSettings) {
const payload = this.getPayload(context)

const mediaType = validationSettings.mediaType
delete validationSettings.mediaType

if (context.eventName === 'check_suite') {
return this.validateCheckSuite(payload, validationSettings)
}

if (context.eventName === 'status') {
return this.validateStatus(context, validationSettings, mediaType)
}

return this.processOptions(validationSettings, payload.base.ref)
}

async validateCheckSuite (payload, validationSettings) {
// A check_suite's payload contains multiple pull_requests
// Need to make sure that each pull_request's base ref is valid
return this.validatePullRequests(payload.pull_requests, validationSettings)
}

async validateStatus (context, validationSettings, mediaType) {
// The commit associated with a status can belong to multiple pull_requests
// Need to make sure that each "open" pull_request's base ref is valid
const request = {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
commit_sha: context.payload.sha
}

if (mediaType) {
request.mediaType = mediaType
}

const pulls = await context.octokit.request(
'GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls',
request
)

const openPullRequests = pulls.data.filter(pullRequest => pullRequest.state === 'open')

return this.validatePullRequests(openPullRequests, validationSettings)
}

async validatePullRequests (pullRequests, validationSettings) {
const validatorContext = { name: 'baseRef' }
const baseRefs = payload.pull_requests.map(pullRequest => pullRequest.base.ref)
const baseRefs = pullRequests.map(pullRequest => pullRequest.base.ref)

// If a check_suite has NO associated pull requests it is considered
// If an event has NO associated pull requests it is considered
// a failed validation since there is no baseRef to validate
if (baseRefs.length === 0) {
return constructOutput({ name: 'baseRef' }, undefined, validationSettings, { status: 'fail', description: 'No pull requests associated with check_suite' })
return constructOutput({ name: 'baseRef' }, undefined, validationSettings, { status: 'fail', description: 'No pull requests associated with event' })
}

const results = await Promise.all(baseRefs.map(
Expand Down

0 comments on commit 45a15e6

Please # to comment.