From 45a15e61ba09b97b30501f3d24a505ec2015e965 Mon Sep 17 00:00:00 2001 From: "Abid H. Mujtaba" Date: Wed, 8 Dec 2021 21:03:30 -0500 Subject: [PATCH] Add support for status events to baseRef validator - 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 --- __tests__/unit/validators/baseRef.test.js | 178 ++++++++++++++++++++++ docs/changelog.rst | 1 + docs/validators/baseRef.rst | 16 +- lib/validators/baseRef.js | 46 +++++- 4 files changed, 236 insertions(+), 5 deletions(-) diff --git a/__tests__/unit/validators/baseRef.test.js b/__tests__/unit/validators/baseRef.test.js index ae15ed69..d409bb60 100644 --- a/__tests__/unit/validators/baseRef.test.js +++ b/__tests__/unit/validators/baseRef.test.js @@ -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() @@ -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 +} diff --git a/docs/changelog.rst b/docs/changelog.rst index c0946123..bfcf7dc8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,6 @@ CHANGELOG ===================================== +| December 12, 2021: feat: Add support for status events to baseRef validator `#395 ` _ | November 25, 2021: feat: Add more supported events to baseRef validator `#395 ` _ | November 12, 2021 : feat: Add baseRef filter `#596 `_ | October 19, 2021 : feat: Add validator approval option to exclude users `#594 `_ diff --git a/docs/validators/baseRef.rst b/docs/validators/baseRef.rst index 7a674baa..9265f93a 100644 --- a/docs/validators/baseRef.rst +++ b/docs/validators/baseRef.rst @@ -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: @@ -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.* diff --git a/lib/validators/baseRef.js b/lib/validators/baseRef.js index f88c0785..86ef513a 100644 --- a/lib/validators/baseRef.js +++ b/lib/validators/baseRef.js @@ -8,7 +8,8 @@ class BaseRef extends Validator { this.supportedEvents = [ 'pull_request.*', 'pull_request_review.*', - 'check_suite.*' + 'check_suite.*', + 'status.*' ] this.supportedSettings = { must_include: { @@ -20,6 +21,9 @@ class BaseRef extends Validator { regex: ['string', 'array'], regex_flag: 'string', message: 'string' + }, + mediaType: { + previews: 'array' } } } @@ -27,23 +31,57 @@ class BaseRef extends Validator { 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(